mirror of
https://github.com/yattee/yattee.git
synced 2025-08-09 20:24:06 +00:00
@@ -189,6 +189,10 @@ extension Defaults.Keys {
|
||||
static let honorSystemOrientationLock = Key<Bool>("honorSystemOrientationLock", default: true)
|
||||
static let enterFullscreenInLandscape = Key<Bool>("enterFullscreenInLandscape", default: UIDevice.current.userInterfaceIdiom == .phone)
|
||||
static let rotateToPortraitOnExitFullScreen = Key<Bool>("rotateToPortraitOnExitFullScreen", default: UIDevice.current.userInterfaceIdiom == .phone)
|
||||
static let rotateToLandscapeOnEnterFullScreen = Key<FullScreenRotationSetting>(
|
||||
"rotateToLandscapeOnEnterFullScreen",
|
||||
default: UIDevice.current.userInterfaceIdiom == .phone ? .landscapeRight : .disabled
|
||||
)
|
||||
#endif
|
||||
|
||||
static let showMPVPlaybackStats = Key<Bool>("showMPVPlaybackStats", default: false)
|
||||
@@ -416,3 +420,24 @@ enum PlayerTapGestureAction: String, CaseIterable, Defaults.Serializable {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum FullScreenRotationSetting: String, CaseIterable, Defaults.Serializable {
|
||||
case disabled
|
||||
case landscapeLeft
|
||||
case landscapeRight
|
||||
|
||||
var interaceOrientation: UIInterfaceOrientation {
|
||||
switch self {
|
||||
case .landscapeLeft:
|
||||
return .landscapeLeft
|
||||
case .landscapeRight:
|
||||
return .landscapeRight
|
||||
default:
|
||||
return .portrait
|
||||
}
|
||||
}
|
||||
|
||||
var isRotating: Bool {
|
||||
self != .disabled
|
||||
}
|
||||
}
|
||||
|
@@ -18,13 +18,23 @@ struct ContentView: View {
|
||||
var body: some View {
|
||||
Group {
|
||||
#if os(iOS)
|
||||
if Constants.isIPhone {
|
||||
AppTabNavigation()
|
||||
} else {
|
||||
if horizontalSizeClass == .compact {
|
||||
AppTabNavigation()
|
||||
} else {
|
||||
AppSidebarNavigation()
|
||||
GeometryReader { proxy in
|
||||
Group {
|
||||
if Constants.isIPhone {
|
||||
AppTabNavigation()
|
||||
} else {
|
||||
if horizontalSizeClass == .compact {
|
||||
AppTabNavigation()
|
||||
} else {
|
||||
AppSidebarNavigation()
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
SafeAreaModel.shared.safeArea = proxy.safeAreaInsets
|
||||
}
|
||||
.onChange(of: proxy.safeAreaInsets) { newValue in
|
||||
SafeAreaModel.shared.safeArea = newValue
|
||||
}
|
||||
}
|
||||
#elseif os(macOS)
|
||||
|
@@ -5,7 +5,7 @@ import SwiftUI
|
||||
#if os(iOS)
|
||||
struct AppleAVPlayerView: UIViewRepresentable {
|
||||
func makeUIView(context _: Context) -> some UIView {
|
||||
PlayerLayerView(frame: .zero)
|
||||
PlayerLayerView()
|
||||
}
|
||||
|
||||
func updateUIView(_: UIViewType, context _: Context) {}
|
||||
|
@@ -13,6 +13,7 @@ struct PlayerControls: View {
|
||||
|
||||
#if os(iOS)
|
||||
@Environment(\.verticalSizeClass) private var verticalSizeClass
|
||||
@ObservedObject private var safeAreaModel = SafeAreaModel.shared
|
||||
#elseif os(tvOS)
|
||||
enum Field: Hashable {
|
||||
case seekOSD
|
||||
@@ -229,7 +230,7 @@ struct PlayerControls: View {
|
||||
guard player.playerSize.height.isFinite else { return 200 }
|
||||
var inset = 0.0
|
||||
#if os(iOS)
|
||||
inset = SafeArea.insets.bottom
|
||||
inset = safeAreaModel.safeArea.bottom
|
||||
#endif
|
||||
return [player.playerSize.height - inset, 500].min()!
|
||||
}
|
||||
|
@@ -5,6 +5,7 @@ struct PlayerBackendView: View {
|
||||
@Environment(\.verticalSizeClass) private var verticalSizeClass
|
||||
#endif
|
||||
@ObservedObject private var player = PlayerModel.shared
|
||||
@ObservedObject private var safeAreaModel = SafeAreaModel.shared
|
||||
|
||||
var body: some View {
|
||||
ZStack(alignment: .top) {
|
||||
@@ -55,19 +56,17 @@ struct PlayerBackendView: View {
|
||||
guard player.playingFullScreen else { return 0 }
|
||||
|
||||
if UIDevice.current.userInterfaceIdiom != .pad {
|
||||
return verticalSizeClass == .compact ? SafeArea.insets.top : 0
|
||||
return verticalSizeClass == .compact ? safeAreaModel.safeArea.top : 0
|
||||
} else {
|
||||
return SafeArea.insets.top.isZero ? SafeArea.insets.bottom : SafeArea.insets.top
|
||||
return safeAreaModel.safeArea.top.isZero ? safeAreaModel.safeArea.bottom : safeAreaModel.safeArea.top
|
||||
}
|
||||
}
|
||||
|
||||
var controlsBottomPadding: Double {
|
||||
guard player.playingFullScreen else { return 0 }
|
||||
|
||||
if UIDevice.current.userInterfaceIdiom != .pad {
|
||||
return player.playingFullScreen && verticalSizeClass == .compact ? SafeArea.insets.bottom : 0
|
||||
return player.playingFullScreen || verticalSizeClass == .compact ? safeAreaModel.safeArea.bottom : 0
|
||||
} else {
|
||||
return player.playingFullScreen ? SafeArea.insets.bottom : 0
|
||||
return player.playingFullScreen ? safeAreaModel.safeArea.bottom : 0
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
@@ -1,77 +0,0 @@
|
||||
import Defaults
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
extension VideoPlayerView {
|
||||
func configureOrientationUpdatesBasedOnAccelerometer() {
|
||||
let currentOrientation = OrientationTracker.shared.currentInterfaceOrientation
|
||||
if currentOrientation.isLandscape,
|
||||
Defaults[.enterFullscreenInLandscape],
|
||||
!Defaults[.honorSystemOrientationLock],
|
||||
!player.playingFullScreen,
|
||||
!player.currentItem.isNil,
|
||||
player.lockedOrientation.isNil || player.lockedOrientation!.contains(.landscape),
|
||||
!player.playingInPictureInPicture,
|
||||
player.presentingPlayer
|
||||
{
|
||||
DispatchQueue.main.async {
|
||||
player.controls.presentingControls = false
|
||||
player.enterFullScreen(showControls: false)
|
||||
}
|
||||
|
||||
player.onPresentPlayer.append {
|
||||
Orientation.lockOrientation(.allButUpsideDown, andRotateTo: currentOrientation)
|
||||
}
|
||||
}
|
||||
|
||||
orientationObserver = NotificationCenter.default.addObserver(
|
||||
forName: OrientationTracker.deviceOrientationChangedNotification,
|
||||
object: nil,
|
||||
queue: .main
|
||||
) { _ in
|
||||
guard !Defaults[.honorSystemOrientationLock],
|
||||
player.presentingPlayer,
|
||||
!player.playingInPictureInPicture,
|
||||
player.lockedOrientation.isNil
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
let orientation = OrientationTracker.shared.currentInterfaceOrientation
|
||||
|
||||
guard lastOrientation != orientation else {
|
||||
return
|
||||
}
|
||||
|
||||
lastOrientation = orientation
|
||||
|
||||
DispatchQueue.main.async {
|
||||
guard Defaults[.enterFullscreenInLandscape],
|
||||
player.presentingPlayer
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
orientationDebouncer.callback = {
|
||||
DispatchQueue.main.async {
|
||||
if orientation.isLandscape {
|
||||
player.controls.presentingControls = false
|
||||
player.enterFullScreen(showControls: false)
|
||||
Orientation.lockOrientation(OrientationTracker.shared.currentInterfaceOrientationMask, andRotateTo: orientation)
|
||||
} else {
|
||||
player.exitFullScreen(showControls: false)
|
||||
Orientation.lockOrientation(.allButUpsideDown, andRotateTo: .portrait)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
orientationDebouncer.call()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func stopOrientationUpdates() {
|
||||
guard let observer = orientationObserver else { return }
|
||||
NotificationCenter.default.removeObserver(observer)
|
||||
}
|
||||
}
|
@@ -37,10 +37,8 @@ struct VideoPlayerView: View {
|
||||
|
||||
#if os(iOS)
|
||||
@Environment(\.verticalSizeClass) private var verticalSizeClass
|
||||
|
||||
@State internal var orientation = UIInterfaceOrientation.portrait
|
||||
@State internal var lastOrientation: UIInterfaceOrientation?
|
||||
@State internal var orientationDebouncer = Debouncer(.milliseconds(300))
|
||||
@ObservedObject private var safeAreaModel = SafeAreaModel.shared
|
||||
private var orientationModel = OrientationModel.shared
|
||||
#elseif os(macOS)
|
||||
var hoverThrottle = Throttle(interval: 0.5)
|
||||
var mouseLocation: CGPoint { NSEvent.mouseLocation }
|
||||
@@ -52,7 +50,6 @@ struct VideoPlayerView: View {
|
||||
@State internal var isHorizontalDrag = false
|
||||
@State internal var isVerticalDrag = false
|
||||
@State internal var viewDragOffset = Self.hiddenOffset
|
||||
@State internal var orientationObserver: Any?
|
||||
#endif
|
||||
|
||||
@ObservedObject internal var player = PlayerModel.shared
|
||||
@@ -101,13 +98,12 @@ struct VideoPlayerView: View {
|
||||
return GeometryReader { geometry in
|
||||
HStack(spacing: 0) {
|
||||
content
|
||||
.ignoresSafeArea(.all, edges: .bottom)
|
||||
.frame(height: playerHeight.isNil ? nil : Double(playerHeight!))
|
||||
.onAppear {
|
||||
playerSize = geometry.size
|
||||
}
|
||||
}
|
||||
#if os(iOS)
|
||||
.padding(.bottom, fullScreenPlayer ? 0.0001 : geometry.safeAreaInsets.bottom)
|
||||
#endif
|
||||
.onChange(of: geometry.size) { _ in
|
||||
self.playerSize = geometry.size
|
||||
}
|
||||
@@ -115,8 +111,6 @@ struct VideoPlayerView: View {
|
||||
player.backend.setNeedsDrawing(!value)
|
||||
}
|
||||
#if os(iOS)
|
||||
.frame(width: playerWidth.isNil ? nil : Double(playerWidth!), height: playerHeight.isNil ? nil : Double(playerHeight!))
|
||||
.ignoresSafeArea(.all, edges: .bottom)
|
||||
.onChange(of: player.presentingPlayer) { newValue in
|
||||
if newValue {
|
||||
viewDragOffset = 0
|
||||
@@ -131,7 +125,7 @@ struct VideoPlayerView: View {
|
||||
viewDragOffset = 0
|
||||
|
||||
Delay.by(0.2) {
|
||||
configureOrientationUpdatesBasedOnAccelerometer()
|
||||
orientationModel.configureOrientationUpdatesBasedOnAccelerometer()
|
||||
|
||||
if let orientationMask = player.lockedOrientation {
|
||||
Orientation.lockOrientation(
|
||||
@@ -149,7 +143,7 @@ struct VideoPlayerView: View {
|
||||
} else {
|
||||
Orientation.lockOrientation(.allButUpsideDown)
|
||||
}
|
||||
stopOrientationUpdates()
|
||||
orientationModel.stopOrientationUpdates()
|
||||
player.controls.hideOverlays()
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in
|
||||
@@ -258,13 +252,10 @@ struct VideoPlayerView: View {
|
||||
dragGestureState && !isHorizontalDrag ? dragGestureOffset.height : viewDragOffset
|
||||
}
|
||||
|
||||
var playerWidth: Double? {
|
||||
fullScreenPlayer ? (UIScreen.main.bounds.size.width - SafeArea.insets.left - SafeArea.insets.right) : nil
|
||||
}
|
||||
|
||||
var playerHeight: Double? {
|
||||
let lockedPortrait = player.lockedOrientation?.contains(.portrait) ?? false
|
||||
return fullScreenPlayer ? UIScreen.main.bounds.size.height - (OrientationTracker.shared.currentInterfaceOrientation.isPortrait || lockedPortrait ? (SafeArea.insets.top + SafeArea.insets.bottom) : 0) : nil
|
||||
let isPortrait = OrientationTracker.shared.currentInterfaceOrientation.isPortrait || lockedPortrait
|
||||
return fullScreenPlayer ? UIScreen.main.bounds.size.height - (isPortrait ? safeAreaModel.safeArea.top + safeAreaModel.safeArea.bottom : 0) : nil
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -282,22 +273,20 @@ struct VideoPlayerView: View {
|
||||
.ignoresSafeArea()
|
||||
#else
|
||||
GeometryReader { geometry in
|
||||
ZStack {
|
||||
player.playerBackendView
|
||||
}
|
||||
.modifier(
|
||||
VideoPlayerSizeModifier(
|
||||
geometry: geometry,
|
||||
aspectRatio: player.aspectRatio,
|
||||
fullScreen: fullScreenPlayer
|
||||
player.playerBackendView
|
||||
|
||||
.modifier(
|
||||
VideoPlayerSizeModifier(
|
||||
geometry: geometry,
|
||||
aspectRatio: player.aspectRatio,
|
||||
fullScreen: fullScreenPlayer
|
||||
)
|
||||
)
|
||||
)
|
||||
.frame(maxWidth: fullScreenPlayer ? .infinity : nil, maxHeight: fullScreenPlayer ? .infinity : nil)
|
||||
.onHover { hovering in
|
||||
hoveringPlayer = hovering
|
||||
hovering ? player.controls.show() : player.controls.hide()
|
||||
}
|
||||
.gesture(player.controls.presentingOverlays ? nil : playerDragGesture)
|
||||
.onHover { hovering in
|
||||
hoveringPlayer = hovering
|
||||
hovering ? player.controls.show() : player.controls.hide()
|
||||
}
|
||||
.gesture(player.controls.presentingOverlays ? nil : playerDragGesture)
|
||||
#if os(macOS)
|
||||
.onAppear(perform: {
|
||||
NSEvent.addLocalMonitorForEvents(matching: [.mouseMoved]) {
|
||||
@@ -338,6 +327,7 @@ struct VideoPlayerView: View {
|
||||
}
|
||||
#endif
|
||||
}
|
||||
.background(BackgroundBlackView().edgesIgnoringSafeArea(.all))
|
||||
.background(((colorScheme == .dark || fullScreenPlayer) ? Color.black : Color.white).edgesIgnoringSafeArea(.all))
|
||||
#if os(macOS)
|
||||
.frame(minWidth: 650)
|
||||
@@ -489,3 +479,16 @@ struct VideoPlayerView_Previews: PreviewProvider {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct BackgroundBlackView: UIViewRepresentable {
|
||||
func makeUIView(context _: Context) -> UIView {
|
||||
let view = UIView()
|
||||
DispatchQueue.main.async {
|
||||
view.superview?.superview?.backgroundColor = .black
|
||||
view.superview?.superview?.layer.removeAllAnimations()
|
||||
}
|
||||
return view
|
||||
}
|
||||
|
||||
func updateUIView(_: UIView, context _: Context) {}
|
||||
}
|
||||
|
@@ -1,25 +0,0 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
struct SafeArea {
|
||||
static var insets: UIEdgeInsets {
|
||||
let keyWindow = scene?.windows.first { $0.isKeyWindow }
|
||||
|
||||
return keyWindow?.safeAreaInsets ?? .init()
|
||||
}
|
||||
|
||||
static var verticalInset: Double {
|
||||
insets.top + insets.bottom
|
||||
}
|
||||
|
||||
static var horizontalInsets: Double {
|
||||
insets.left + insets.right
|
||||
}
|
||||
|
||||
static var scene: UIWindowScene? {
|
||||
UIApplication.shared.connectedScenes
|
||||
.filter { $0.activationState == .foregroundActive }
|
||||
.compactMap { $0 as? UIWindowScene }
|
||||
.first
|
||||
}
|
||||
}
|
@@ -17,6 +17,7 @@ struct PlayerSettings: View {
|
||||
@Default(.honorSystemOrientationLock) private var honorSystemOrientationLock
|
||||
@Default(.enterFullscreenInLandscape) private var enterFullscreenInLandscape
|
||||
@Default(.rotateToPortraitOnExitFullScreen) private var rotateToPortraitOnExitFullScreen
|
||||
@Default(.rotateToLandscapeOnEnterFullScreen) private var rotateToLandscapeOnEnterFullScreen
|
||||
#endif
|
||||
@Default(.closePiPOnNavigation) private var closePiPOnNavigation
|
||||
@Default(.closePiPOnOpeningPlayer) private var closePiPOnOpeningPlayer
|
||||
@@ -118,8 +119,9 @@ struct PlayerSettings: View {
|
||||
if idiom == .pad {
|
||||
enterFullscreenInLandscapeToggle
|
||||
}
|
||||
rotateToPortraitOnExitFullScreenToggle
|
||||
honorSystemOrientationLockToggle
|
||||
rotateToPortraitOnExitFullScreenToggle
|
||||
rotateToLandscapeOnEnterFullScreenPicker
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -209,6 +211,15 @@ struct PlayerSettings: View {
|
||||
private var rotateToPortraitOnExitFullScreenToggle: some View {
|
||||
Toggle("Rotate to portrait when exiting fullscreen", isOn: $rotateToPortraitOnExitFullScreen)
|
||||
}
|
||||
|
||||
private var rotateToLandscapeOnEnterFullScreenPicker: some View {
|
||||
Picker("Rotate when entering fullscreen on landscape video", selection: $rotateToLandscapeOnEnterFullScreen) {
|
||||
Text("Landscape left").tag(FullScreenRotationSetting.landscapeRight)
|
||||
Text("Landscape right").tag(FullScreenRotationSetting.landscapeLeft)
|
||||
Text("No rotation").tag(FullScreenRotationSetting.disabled)
|
||||
}
|
||||
.modifier(SettingsPickerModifier())
|
||||
}
|
||||
#endif
|
||||
|
||||
private var closePiPOnNavigationToggle: some View {
|
||||
|
Reference in New Issue
Block a user