mirror of
https://github.com/yattee/yattee.git
synced 2024-12-22 21:43:41 +00:00
Add iOS options for handling landscape fullscreen (fixes #38)
This commit is contained in:
parent
d6e75295e1
commit
00778b585f
@ -1,15 +1,18 @@
|
|||||||
import AVKit
|
import AVKit
|
||||||
import CoreData
|
import CoreData
|
||||||
|
#if os(iOS)
|
||||||
|
import CoreMotion
|
||||||
|
#endif
|
||||||
import Defaults
|
import Defaults
|
||||||
import Foundation
|
import Foundation
|
||||||
import Logging
|
import Logging
|
||||||
import MediaPlayer
|
import MediaPlayer
|
||||||
#if !os(macOS)
|
|
||||||
import UIKit
|
|
||||||
#endif
|
|
||||||
import Siesta
|
import Siesta
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import SwiftyJSON
|
import SwiftyJSON
|
||||||
|
#if !os(macOS)
|
||||||
|
import UIKit
|
||||||
|
#endif
|
||||||
|
|
||||||
final class PlayerModel: ObservableObject {
|
final class PlayerModel: ObservableObject {
|
||||||
static let availableRates: [Float] = [0.5, 0.67, 0.8, 1, 1.25, 1.5, 2]
|
static let availableRates: [Float] = [0.5, 0.67, 0.8, 1, 1.25, 1.5, 2]
|
||||||
@ -44,6 +47,12 @@ final class PlayerModel: ObservableObject {
|
|||||||
|
|
||||||
@Published var channelWithDetails: Channel?
|
@Published var channelWithDetails: Channel?
|
||||||
|
|
||||||
|
#if os(iOS)
|
||||||
|
@Published var motionManager: CMMotionManager!
|
||||||
|
@Published var lockedOrientation: UIInterfaceOrientation?
|
||||||
|
@Published var lastOrientation: UIInterfaceOrientation?
|
||||||
|
#endif
|
||||||
|
|
||||||
var accounts: AccountsModel
|
var accounts: AccountsModel
|
||||||
var comments: CommentsModel
|
var comments: CommentsModel
|
||||||
|
|
||||||
@ -63,6 +72,7 @@ final class PlayerModel: ObservableObject {
|
|||||||
private var timeObserverThrottle = Throttle(interval: 2)
|
private var timeObserverThrottle = Throttle(interval: 2)
|
||||||
|
|
||||||
var playingInPictureInPicture = false
|
var playingInPictureInPicture = false
|
||||||
|
var playingFullscreen = false
|
||||||
|
|
||||||
@Published var presentingErrorDetails = false
|
@Published var presentingErrorDetails = false
|
||||||
var playerError: Error? { didSet {
|
var playerError: Error? { didSet {
|
||||||
@ -105,11 +115,8 @@ final class PlayerModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func hide() {
|
func hide() {
|
||||||
guard presentingPlayer else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
presentingPlayer = false
|
presentingPlayer = false
|
||||||
|
playerNavigationLinkActive = false
|
||||||
}
|
}
|
||||||
|
|
||||||
func togglePlayer() {
|
func togglePlayer() {
|
||||||
@ -388,7 +395,9 @@ final class PlayerModel: ObservableObject {
|
|||||||
self?.insertPlayerItem(stream, for: video, preservingTime: preservingTime)
|
self?.insertPlayerItem(stream, for: video, preservingTime: preservingTime)
|
||||||
}
|
}
|
||||||
case .failed:
|
case .failed:
|
||||||
self?.playerError = error
|
DispatchQueue.main.async { [weak self] in
|
||||||
|
self?.playerError = error
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -808,5 +817,27 @@ final class PlayerModel: ObservableObject {
|
|||||||
show()
|
show()
|
||||||
closePiP()
|
closePiP()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func enterFullScreen() {
|
||||||
|
guard !playingFullscreen else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("entering fullscreen")
|
||||||
|
|
||||||
|
controller?.playerView
|
||||||
|
.perform(NSSelectorFromString("enterFullScreenAnimated:completionHandler:"), with: false, with: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func exitFullScreen() {
|
||||||
|
guard playingFullscreen else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("exiting fullscreen")
|
||||||
|
|
||||||
|
controller?.playerView
|
||||||
|
.perform(NSSelectorFromString("exitFullScreenAnimated:completionHandler:"), with: false, with: nil)
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
@ -82,6 +82,12 @@ extension Defaults.Keys {
|
|||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
static let enableBetaChannel = Key<Bool>("enableBetaChannel", default: false)
|
static let enableBetaChannel = Key<Bool>("enableBetaChannel", default: false)
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#if os(iOS)
|
||||||
|
static let honorSystemOrientationLock = Key<Bool>("honorSystemOrientationLock", default: true)
|
||||||
|
static let enterFullscreenInLandscape = Key<Bool>("enterFullscreenInLandscape", default: UIDevice.current.userInterfaceIdiom == .phone)
|
||||||
|
static let lockLandscapeWhenEnteringFullscreen = Key<Bool>("lockLandscapeWhenEnteringFullscreen", default: false)
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ResolutionSetting: String, CaseIterable, Defaults.Serializable {
|
enum ResolutionSetting: String, CaseIterable, Defaults.Serializable {
|
||||||
|
@ -132,17 +132,32 @@ extension PlayerViewController: AVPlayerViewControllerDelegate {
|
|||||||
func playerViewController(
|
func playerViewController(
|
||||||
_: AVPlayerViewController,
|
_: AVPlayerViewController,
|
||||||
willBeginFullScreenPresentationWithAnimationCoordinator _: UIViewControllerTransitionCoordinator
|
willBeginFullScreenPresentationWithAnimationCoordinator _: UIViewControllerTransitionCoordinator
|
||||||
) {}
|
) {
|
||||||
|
playerModel.playingFullscreen = true
|
||||||
|
}
|
||||||
|
|
||||||
func playerViewController(
|
func playerViewController(
|
||||||
_: AVPlayerViewController,
|
_: AVPlayerViewController,
|
||||||
willEndFullScreenPresentationWithAnimationCoordinator coordinator: UIViewControllerTransitionCoordinator
|
willEndFullScreenPresentationWithAnimationCoordinator coordinator: UIViewControllerTransitionCoordinator
|
||||||
) {
|
) {
|
||||||
|
let wasPlaying = playerModel.isPlaying
|
||||||
coordinator.animate(alongsideTransition: nil) { context in
|
coordinator.animate(alongsideTransition: nil) { context in
|
||||||
|
#if os(iOS)
|
||||||
|
if wasPlaying {
|
||||||
|
self.playerModel.play()
|
||||||
|
}
|
||||||
|
#endif
|
||||||
if !context.isCancelled {
|
if !context.isCancelled {
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
if self.traitCollection.verticalSizeClass == .compact {
|
self.playerModel.lockedOrientation = nil
|
||||||
self.dismiss(animated: true)
|
if Defaults[.enterFullscreenInLandscape] {
|
||||||
|
Orientation.lockOrientation(.portrait, andRotateTo: .portrait)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.playerModel.playingFullscreen = false
|
||||||
|
|
||||||
|
if wasPlaying {
|
||||||
|
self.playerModel.play()
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
import AVKit
|
import AVKit
|
||||||
|
#if os(iOS)
|
||||||
|
import CoreMotion
|
||||||
|
#endif
|
||||||
import Defaults
|
import Defaults
|
||||||
import Siesta
|
import Siesta
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
@ -22,6 +25,16 @@ struct VideoPlayerView: View {
|
|||||||
@Environment(\.presentationMode) private var presentationMode
|
@Environment(\.presentationMode) private var presentationMode
|
||||||
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
||||||
@Environment(\.verticalSizeClass) private var verticalSizeClass
|
@Environment(\.verticalSizeClass) private var verticalSizeClass
|
||||||
|
|
||||||
|
@Default(.enterFullscreenInLandscape) private var enterFullscreenInLandscape
|
||||||
|
@Default(.honorSystemOrientationLock) private var honorSystemOrientationLock
|
||||||
|
@Default(.lockLandscapeWhenEnteringFullscreen) private var lockLandscapeWhenEnteringFullscreen
|
||||||
|
|
||||||
|
@State private var motionManager: CMMotionManager!
|
||||||
|
@State private var orientation = UIInterfaceOrientation.portrait
|
||||||
|
@State private var lastOrientation: UIInterfaceOrientation?
|
||||||
|
|
||||||
|
private var orientationThrottle = Throttle(interval: 2)
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@EnvironmentObject<AccountsModel> private var accounts
|
@EnvironmentObject<AccountsModel> private var accounts
|
||||||
@ -38,13 +51,36 @@ struct VideoPlayerView: View {
|
|||||||
GeometryReader { geometry in
|
GeometryReader { geometry in
|
||||||
HStack(spacing: 0) {
|
HStack(spacing: 0) {
|
||||||
content
|
content
|
||||||
}
|
.onAppear {
|
||||||
.onAppear {
|
playerSize = geometry.size
|
||||||
self.playerSize = geometry.size
|
|
||||||
|
#if os(iOS)
|
||||||
|
configureOrientationUpdatesBasedOnAccelerometer()
|
||||||
|
#endif
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.onChange(of: geometry.size) { size in
|
.onChange(of: geometry.size) { size in
|
||||||
self.playerSize = size
|
self.playerSize = size
|
||||||
}
|
}
|
||||||
|
#if os(iOS)
|
||||||
|
.onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in
|
||||||
|
handleOrientationDidChangeNotification()
|
||||||
|
}
|
||||||
|
.onDisappear {
|
||||||
|
guard !player.playingFullscreen else {
|
||||||
|
return // swiftlint:disable:this implicit_return
|
||||||
|
}
|
||||||
|
|
||||||
|
if Defaults[.lockPortraitWhenBrowsing] {
|
||||||
|
Orientation.lockOrientation(.portrait, andRotateTo: .portrait)
|
||||||
|
} else {
|
||||||
|
Orientation.lockOrientation(.allButUpsideDown)
|
||||||
|
}
|
||||||
|
|
||||||
|
motionManager?.stopAccelerometerUpdates()
|
||||||
|
motionManager = nil
|
||||||
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
.navigationBarHidden(true)
|
.navigationBarHidden(true)
|
||||||
#endif
|
#endif
|
||||||
@ -192,6 +228,110 @@ struct VideoPlayerView: View {
|
|||||||
set: { _ in }
|
set: { _ in }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if os(iOS)
|
||||||
|
private func configureOrientationUpdatesBasedOnAccelerometer() {
|
||||||
|
if UIDevice.current.orientation.isLandscape, enterFullscreenInLandscape, !player.playingFullscreen {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
player.enterFullScreen()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
guard !honorSystemOrientationLock, motionManager.isNil else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
motionManager = CMMotionManager()
|
||||||
|
motionManager.accelerometerUpdateInterval = 0.2
|
||||||
|
motionManager.startAccelerometerUpdates(to: OperationQueue()) { data, _ in
|
||||||
|
guard player.presentingPlayer, !data.isNil else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let acceleration = data?.acceleration else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var orientation = UIInterfaceOrientation.unknown
|
||||||
|
|
||||||
|
if acceleration.x >= 0.65 {
|
||||||
|
orientation = .landscapeLeft
|
||||||
|
} else if acceleration.x <= -0.65 {
|
||||||
|
orientation = .landscapeRight
|
||||||
|
} else if acceleration.y <= -0.65 {
|
||||||
|
orientation = .portrait
|
||||||
|
} else if acceleration.y >= 0.65 {
|
||||||
|
orientation = .portraitUpsideDown
|
||||||
|
}
|
||||||
|
|
||||||
|
guard lastOrientation != orientation else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
lastOrientation = orientation
|
||||||
|
|
||||||
|
if orientation.isLandscape {
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.8) {
|
||||||
|
guard enterFullscreenInLandscape else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
player.enterFullScreen()
|
||||||
|
|
||||||
|
let orientationLockMask = orientation == .landscapeLeft ? UIInterfaceOrientationMask.landscapeLeft : .landscapeRight
|
||||||
|
|
||||||
|
Orientation.lockOrientation(orientationLockMask, andRotateTo: orientation)
|
||||||
|
|
||||||
|
guard lockLandscapeWhenEnteringFullscreen else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
player.lockedOrientation = orientation
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
guard abs(acceleration.z) <= 0.74,
|
||||||
|
player.lockedOrientation.isNil,
|
||||||
|
enterFullscreenInLandscape
|
||||||
|
else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
|
||||||
|
player.exitFullScreen()
|
||||||
|
}
|
||||||
|
|
||||||
|
Orientation.lockOrientation(.portrait)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func handleOrientationDidChangeNotification() {
|
||||||
|
let newOrientation = UIApplication.shared.windows.first?.windowScene?.interfaceOrientation
|
||||||
|
if newOrientation?.isLandscape ?? false, player.presentingPlayer, lockLandscapeWhenEnteringFullscreen, !player.lockedOrientation.isNil {
|
||||||
|
Orientation.lockOrientation(.landscape, andRotateTo: newOrientation)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard player.presentingPlayer, enterFullscreenInLandscape, honorSystemOrientationLock else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if UIDevice.current.orientation.isLandscape {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
player.lockedOrientation = newOrientation
|
||||||
|
player.enterFullScreen()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
player.exitFullScreen()
|
||||||
|
}
|
||||||
|
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.8) {
|
||||||
|
player.exitFullScreen()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
struct VideoPlayerView_Previews: PreviewProvider {
|
struct VideoPlayerView_Previews: PreviewProvider {
|
||||||
|
@ -10,6 +10,11 @@ struct PlaybackSettings: View {
|
|||||||
@Default(.showKeywords) private var showKeywords
|
@Default(.showKeywords) private var showKeywords
|
||||||
@Default(.showChannelSubscribers) private var channelSubscribers
|
@Default(.showChannelSubscribers) private var channelSubscribers
|
||||||
@Default(.pauseOnHidingPlayer) private var pauseOnHidingPlayer
|
@Default(.pauseOnHidingPlayer) private var pauseOnHidingPlayer
|
||||||
|
#if os(iOS)
|
||||||
|
@Default(.honorSystemOrientationLock) private var honorSystemOrientationLock
|
||||||
|
@Default(.lockLandscapeWhenEnteringFullscreen) private var lockLandscapeWhenEnteringFullscreen
|
||||||
|
@Default(.enterFullscreenInLandscape) private var enterFullscreenInLandscape
|
||||||
|
#endif
|
||||||
@Default(.closePiPOnNavigation) private var closePiPOnNavigation
|
@Default(.closePiPOnNavigation) private var closePiPOnNavigation
|
||||||
@Default(.closePiPOnOpeningPlayer) private var closePiPOnOpeningPlayer
|
@Default(.closePiPOnOpeningPlayer) private var closePiPOnOpeningPlayer
|
||||||
#if !os(macOS)
|
#if !os(macOS)
|
||||||
@ -37,6 +42,13 @@ struct PlaybackSettings: View {
|
|||||||
showHistoryToggle
|
showHistoryToggle
|
||||||
channelSubscribersToggle
|
channelSubscribersToggle
|
||||||
pauseOnHidingPlayerToggle
|
pauseOnHidingPlayerToggle
|
||||||
|
|
||||||
|
if idiom == .pad {
|
||||||
|
enterFullscreenInLandscapeToggle
|
||||||
|
}
|
||||||
|
|
||||||
|
honorSystemOrientationLockToggle
|
||||||
|
lockLandscapeWhenEnteringFullscreenToggle
|
||||||
}
|
}
|
||||||
|
|
||||||
Section(header: SettingsHeader(text: "Picture in Picture")) {
|
Section(header: SettingsHeader(text: "Picture in Picture")) {
|
||||||
@ -147,6 +159,22 @@ struct PlaybackSettings: View {
|
|||||||
Toggle("Pause when player is closed", isOn: $pauseOnHidingPlayer)
|
Toggle("Pause when player is closed", isOn: $pauseOnHidingPlayer)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if os(iOS)
|
||||||
|
private var honorSystemOrientationLockToggle: some View {
|
||||||
|
Toggle("Honor system orientation lock", isOn: $honorSystemOrientationLock)
|
||||||
|
.disabled(!enterFullscreenInLandscape)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var enterFullscreenInLandscapeToggle: some View {
|
||||||
|
Toggle("Enter fullscreen in landscape", isOn: $enterFullscreenInLandscape)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var lockLandscapeWhenEnteringFullscreenToggle: some View {
|
||||||
|
Toggle("Lock landscape orientation when entering fullscreen", isOn: $lockLandscapeWhenEnteringFullscreen)
|
||||||
|
.disabled(!enterFullscreenInLandscape)
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
private var closePiPOnNavigationToggle: some View {
|
private var closePiPOnNavigationToggle: some View {
|
||||||
Toggle("Close PiP when starting playing other video", isOn: $closePiPOnNavigation)
|
Toggle("Close PiP when starting playing other video", isOn: $closePiPOnNavigation)
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user