From f67b1d4febf0776f91c49dbe3a40600199eeb924 Mon Sep 17 00:00:00 2001 From: Arkadiusz Fal Date: Sat, 20 May 2023 16:04:58 +0200 Subject: [PATCH] Improve orientation and safe area handling Fix #369 Fix #382 --- Model/Player/Backends/MPVClient.swift | 14 ++-- Model/Player/PlayerModel.swift | 30 +++++---- Shared/Defaults.swift | 25 +++++++ Shared/Navigation/ContentView.swift | 24 +++++-- Shared/Player/AppleAVPlayerView.swift | 2 +- Shared/Player/Controls/PlayerControls.swift | 3 +- Shared/Player/PlayerBackendView.swift | 11 ++- Shared/Player/VideoPlayerView.swift | 67 ++++++++++--------- Shared/SafeArea.swift | 25 ------- Shared/Settings/PlayerSettings.swift | 13 +++- Yattee.xcodeproj/project.pbxproj | 18 +++-- iOS/Orientation.swift | 9 ++- .../OrientationModel.swift | 46 +++++++++---- iOS/SafeAreaModel.swift | 7 ++ 14 files changed, 176 insertions(+), 118 deletions(-) delete mode 100644 Shared/SafeArea.swift rename Shared/Player/PlayerOrientation.swift => iOS/OrientationModel.swift (58%) create mode 100644 iOS/SafeAreaModel.swift diff --git a/Model/Player/Backends/MPVClient.swift b/Model/Player/Backends/MPVClient.swift index 8f9658b1..7d98b965 100644 --- a/Model/Player/Backends/MPVClient.swift +++ b/Model/Player/Backends/MPVClient.swift @@ -318,14 +318,14 @@ final class MPVClient: ObservableObject { DispatchQueue.main.async { [weak self] in guard let self else { return } let model = self.backend.model + let aspectRatio = self.aspectRatio > 0 && self.aspectRatio < VideoPlayerView.defaultAspectRatio ? self.aspectRatio : VideoPlayerView.defaultAspectRatio + let height = [model.playerSize.height, model.playerSize.width / aspectRatio].min()! + var insets = 0.0 + #if os(iOS) + insets = OrientationTracker.shared.currentInterfaceOrientation.isPortrait ? SafeAreaModel.shared.safeArea.bottom : 0 + #endif + let offsetY = max(0, model.playingFullScreen ? ((model.playerSize.height / 2.0) - ((height + insets) / 2)) : 0) UIView.animate(withDuration: 0.2, animations: { - let aspectRatio = self.aspectRatio > 0 && self.aspectRatio < VideoPlayerView.defaultAspectRatio ? self.aspectRatio : VideoPlayerView.defaultAspectRatio - let height = [model.playerSize.height, model.playerSize.width / aspectRatio].min()! - var insets = 0.0 - #if os(iOS) - insets = OrientationTracker.shared.currentInterfaceOrientation.isPortrait ? SafeArea.insets.bottom : 0 - #endif - let offsetY = model.playingFullScreen ? ((model.playerSize.height / 2.0) - ((height + insets) / 2)) : 0 self.glView?.frame = CGRect(x: 0, y: offsetY, width: roundedWidth, height: height) }) { completion in if completion { diff --git a/Model/Player/PlayerModel.swift b/Model/Player/PlayerModel.swift index bb1e1fc3..c681a2b8 100644 --- a/Model/Player/PlayerModel.swift +++ b/Model/Player/PlayerModel.swift @@ -129,6 +129,7 @@ final class PlayerModel: ObservableObject { #if os(iOS) @Published var lockedOrientation: UIInterfaceOrientationMask? @Default(.rotateToPortraitOnExitFullScreen) private var rotateToPortraitOnExitFullScreen + @Default(.rotateToLandscapeOnEnterFullScreen) private var rotateToLandscapeOnEnterFullScreen #endif var accounts: AccountsModel { .shared } @@ -981,24 +982,21 @@ final class PlayerModel: ObservableObject { Windows.player.toggleFullScreen() #endif + playingFullScreen = !isFullScreen + #if os(iOS) - if !playingFullScreen { - playingFullScreen = true - Orientation.lockOrientation(.allButUpsideDown) + if playingFullScreen { + guard rotateToLandscapeOnEnterFullScreen.isRotating else { return } + if currentVideoIsLandscape { + // not sure why but first rotation call is ignore so doing rotate to same orientation first + Orientation.lockOrientation(.allButUpsideDown, andRotateTo: OrientationTracker.shared.currentInterfaceOrientation) + Orientation.lockOrientation(.allButUpsideDown, andRotateTo: rotateToLandscapeOnEnterFullScreen.interaceOrientation) + } } else { let rotationOrientation = rotateToPortraitOnExitFullScreen ? UIInterfaceOrientation.portrait : nil Orientation.lockOrientation(.allButUpsideDown, andRotateTo: rotationOrientation) - // TODO: rework to move view before rotating - if SafeArea.insets.left > 0 { - Delay.by(0.15) { - self.playingFullScreen = false - } - } else { - self.playingFullScreen = false - } } - #else - playingFullScreen = !isFullScreen + #endif } @@ -1036,6 +1034,12 @@ final class PlayerModel: ObservableObject { #endif } + var currentVideoIsLandscape: Bool { + guard currentVideo != nil else { return false } + + return aspectRatio > 1 + } + var formattedSize: String { guard let videoWidth = backend?.videoWidth, let videoHeight = backend?.videoHeight else { return "unknown" } return "\(String(format: "%.2f", videoWidth))\u{d7}\(String(format: "%.2f", videoHeight))" diff --git a/Shared/Defaults.swift b/Shared/Defaults.swift index 4afa7c6a..3453de7c 100644 --- a/Shared/Defaults.swift +++ b/Shared/Defaults.swift @@ -189,6 +189,10 @@ extension Defaults.Keys { static let honorSystemOrientationLock = Key("honorSystemOrientationLock", default: true) static let enterFullscreenInLandscape = Key("enterFullscreenInLandscape", default: UIDevice.current.userInterfaceIdiom == .phone) static let rotateToPortraitOnExitFullScreen = Key("rotateToPortraitOnExitFullScreen", default: UIDevice.current.userInterfaceIdiom == .phone) + static let rotateToLandscapeOnEnterFullScreen = Key( + "rotateToLandscapeOnEnterFullScreen", + default: UIDevice.current.userInterfaceIdiom == .phone ? .landscapeRight : .disabled + ) #endif static let showMPVPlaybackStats = Key("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 + } +} diff --git a/Shared/Navigation/ContentView.swift b/Shared/Navigation/ContentView.swift index 60e05ce8..e69fcdba 100644 --- a/Shared/Navigation/ContentView.swift +++ b/Shared/Navigation/ContentView.swift @@ -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) diff --git a/Shared/Player/AppleAVPlayerView.swift b/Shared/Player/AppleAVPlayerView.swift index bdbd9ece..0fd72bea 100644 --- a/Shared/Player/AppleAVPlayerView.swift +++ b/Shared/Player/AppleAVPlayerView.swift @@ -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) {} diff --git a/Shared/Player/Controls/PlayerControls.swift b/Shared/Player/Controls/PlayerControls.swift index 9e64a8c3..1a3c57de 100644 --- a/Shared/Player/Controls/PlayerControls.swift +++ b/Shared/Player/Controls/PlayerControls.swift @@ -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()! } diff --git a/Shared/Player/PlayerBackendView.swift b/Shared/Player/PlayerBackendView.swift index c5f33002..c47df52c 100644 --- a/Shared/Player/PlayerBackendView.swift +++ b/Shared/Player/PlayerBackendView.swift @@ -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 diff --git a/Shared/Player/VideoPlayerView.swift b/Shared/Player/VideoPlayerView.swift index 419c8d38..04ef3812 100644 --- a/Shared/Player/VideoPlayerView.swift +++ b/Shared/Player/VideoPlayerView.swift @@ -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) {} +} diff --git a/Shared/SafeArea.swift b/Shared/SafeArea.swift deleted file mode 100644 index 2cbb8b5a..00000000 --- a/Shared/SafeArea.swift +++ /dev/null @@ -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 - } -} diff --git a/Shared/Settings/PlayerSettings.swift b/Shared/Settings/PlayerSettings.swift index 8da1fe78..b4432012 100644 --- a/Shared/Settings/PlayerSettings.swift +++ b/Shared/Settings/PlayerSettings.swift @@ -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 { diff --git a/Yattee.xcodeproj/project.pbxproj b/Yattee.xcodeproj/project.pbxproj index c2e0f88c..42b29407 100644 --- a/Yattee.xcodeproj/project.pbxproj +++ b/Yattee.xcodeproj/project.pbxproj @@ -253,7 +253,6 @@ 37319F0627103F94004ECCD0 /* PlayerQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37319F0427103F94004ECCD0 /* PlayerQueue.swift */; }; 37319F0727103F94004ECCD0 /* PlayerQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37319F0427103F94004ECCD0 /* PlayerQueue.swift */; }; 3732BFD028B83763009F3F4D /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = 3732BFCF28B83763009F3F4D /* KeychainAccess */; }; - 3732C9FD28C012E600E7DCAF /* SafeArea.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37ECED55289FE166002BC2C9 /* SafeArea.swift */; }; 3735C4E729A2D3D70051D251 /* Logging in Frameworks */ = {isa = PBXBuildFile; productRef = 3735C4E629A2D3D70051D251 /* Logging */; }; 3736A1FE286BB72300C9E5EE /* libavdevice.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3736A1EF286BB72300C9E5EE /* libavdevice.xcframework */; }; 3736A1FF286BB72300C9E5EE /* libavdevice.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3736A1EF286BB72300C9E5EE /* libavdevice.xcframework */; }; @@ -375,7 +374,6 @@ 374D11E72943C56300CB4350 /* Cache in Frameworks */ = {isa = PBXBuildFile; productRef = 374D11E62943C56300CB4350 /* Cache */; }; 374DE88028BB896C0062BBF2 /* PlayerDragGesture.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374DE87F28BB896C0062BBF2 /* PlayerDragGesture.swift */; }; 374DE88128BB896C0062BBF2 /* PlayerDragGesture.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374DE87F28BB896C0062BBF2 /* PlayerDragGesture.swift */; }; - 374DE88328BB8A280062BBF2 /* PlayerOrientation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374DE88228BB8A280062BBF2 /* PlayerOrientation.swift */; }; 375168D62700FAFF008F96A6 /* Debounce.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375168D52700FAFF008F96A6 /* Debounce.swift */; }; 375168D72700FDB8008F96A6 /* Debounce.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375168D52700FAFF008F96A6 /* Debounce.swift */; }; 375168D82700FDB9008F96A6 /* Debounce.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375168D52700FAFF008F96A6 /* Debounce.swift */; }; @@ -885,6 +883,8 @@ 37D9BA0629507F69002586BD /* PlayerControlsSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37D9BA0529507F69002586BD /* PlayerControlsSettings.swift */; }; 37D9BA0729507F69002586BD /* PlayerControlsSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37D9BA0529507F69002586BD /* PlayerControlsSettings.swift */; }; 37D9BA0829507F69002586BD /* PlayerControlsSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37D9BA0529507F69002586BD /* PlayerControlsSettings.swift */; }; + 37DCD3112A18E8150059A470 /* OrientationModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37DCD3102A18E8150059A470 /* OrientationModel.swift */; }; + 37DCD3152A18F7630059A470 /* SafeAreaModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37DCD3142A18F7630059A470 /* SafeAreaModel.swift */; }; 37DD87C7271C9CFE0027CBF9 /* PlayerStreams.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37DD87C6271C9CFE0027CBF9 /* PlayerStreams.swift */; }; 37DD87C8271C9CFE0027CBF9 /* PlayerStreams.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37DD87C6271C9CFE0027CBF9 /* PlayerStreams.swift */; }; 37DD87C9271C9CFE0027CBF9 /* PlayerStreams.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37DD87C6271C9CFE0027CBF9 /* PlayerStreams.swift */; }; @@ -941,7 +941,6 @@ 37EBD8CA27AF26C200F1C24B /* MPVBackend.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37EBD8C927AF26C200F1C24B /* MPVBackend.swift */; }; 37EBD8CB27AF26C200F1C24B /* MPVBackend.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37EBD8C927AF26C200F1C24B /* MPVBackend.swift */; }; 37EBD8CC27AF26C200F1C24B /* MPVBackend.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37EBD8C927AF26C200F1C24B /* MPVBackend.swift */; }; - 37ECED56289FE166002BC2C9 /* SafeArea.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37ECED55289FE166002BC2C9 /* SafeArea.swift */; }; 37EE6DC528A305AD00BFD632 /* Reachability in Frameworks */ = {isa = PBXBuildFile; productRef = 37EE6DC428A305AD00BFD632 /* Reachability */; }; 37EF5C222739D37B00B03725 /* MenuModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37EF5C212739D37B00B03725 /* MenuModel.swift */; }; 37EF5C232739D37B00B03725 /* MenuModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37EF5C212739D37B00B03725 /* MenuModel.swift */; }; @@ -1258,7 +1257,6 @@ 374C0542272496E4009BDDBE /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = macOS/AppDelegate.swift; sourceTree = SOURCE_ROOT; }; 374C0544272496FD009BDDBE /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 374DE87F28BB896C0062BBF2 /* PlayerDragGesture.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerDragGesture.swift; sourceTree = ""; }; - 374DE88228BB8A280062BBF2 /* PlayerOrientation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerOrientation.swift; sourceTree = ""; }; 375168D52700FAFF008F96A6 /* Debounce.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Debounce.swift; sourceTree = ""; }; 3751B4B127836902000B7DF4 /* SearchPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchPage.swift; sourceTree = ""; }; 3751BA7D27E63F1D007B1A60 /* MPVOGLView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPVOGLView.swift; sourceTree = ""; }; @@ -1458,6 +1456,8 @@ 37D836BB294927E700005E5E /* ChannelsCacheModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelsCacheModel.swift; sourceTree = ""; }; 37D9169A27388A81002B1BAA /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 37D9BA0529507F69002586BD /* PlayerControlsSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerControlsSettings.swift; sourceTree = ""; }; + 37DCD3102A18E8150059A470 /* OrientationModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrientationModel.swift; sourceTree = ""; }; + 37DCD3142A18F7630059A470 /* SafeAreaModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafeAreaModel.swift; sourceTree = ""; }; 37DD87C6271C9CFE0027CBF9 /* PlayerStreams.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerStreams.swift; sourceTree = ""; }; 37DD9DA22785BBC900539416 /* NoCommentsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoCommentsView.swift; sourceTree = ""; }; 37DD9DAF2785D58D00539416 /* RefreshControl.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RefreshControl.swift; sourceTree = ""; }; @@ -1485,7 +1485,6 @@ 37EBD8C327AF0DA800F1C24B /* PlayerBackend.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerBackend.swift; sourceTree = ""; }; 37EBD8C527AF26B300F1C24B /* AVPlayerBackend.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVPlayerBackend.swift; sourceTree = ""; }; 37EBD8C927AF26C200F1C24B /* MPVBackend.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPVBackend.swift; sourceTree = ""; }; - 37ECED55289FE166002BC2C9 /* SafeArea.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafeArea.swift; sourceTree = ""; }; 37EF5C212739D37B00B03725 /* MenuModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuModel.swift; sourceTree = ""; }; 37EF9A75275BEB8E0043B585 /* CommentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentView.swift; sourceTree = ""; }; 37EFAC0728C138CD00ED9B89 /* ControlsOverlayModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControlsOverlayModel.swift; sourceTree = ""; }; @@ -1812,7 +1811,6 @@ 374DE87F28BB896C0062BBF2 /* PlayerDragGesture.swift */, 3703100127B0713600ECDDAA /* PlayerGestures.swift */, 373031F22838388A000CFD59 /* PlayerLayerView.swift */, - 374DE88228BB8A280062BBF2 /* PlayerOrientation.swift */, 3743CA4D270EFE3400E4D32B /* PlayerQueueRow.swift */, 373197D82732015300EF734F /* RelatedView.swift */, 3795593527B08538007FF8F4 /* StreamControl.swift */, @@ -2180,6 +2178,8 @@ 3749BF9227ADA142000480FF /* BridgingHeader.h */, 37992DC726CC50BC003D4C27 /* Info.plist */, 3782430A291E5AFA005DEC1C /* Yattee (iOS).entitlements */, + 37DCD3102A18E8150059A470 /* OrientationModel.swift */, + 37DCD3142A18F7630059A470 /* SafeAreaModel.swift */, ); path = iOS; sourceTree = ""; @@ -2298,7 +2298,6 @@ 3729037D2739E47400EA99F6 /* MenuCommands.swift */, 37B7958F2771DAE0001CF27B /* OpenURLHandler.swift */, 374924EC2921669B0017D862 /* PreferenceKeys.swift */, - 37ECED55289FE166002BC2C9 /* SafeArea.swift */, 3700155E271B12DD0049C794 /* SiestaConfiguration.swift */, 37FFC43F272734C3009FFD26 /* Throttle.swift */, 378FFBC328660172009E3FBE /* URLParser.swift */, @@ -3057,7 +3056,6 @@ 374710052755291C00CE0F87 /* SearchTextField.swift in Sources */, 37494EA529200B14000DF176 /* DocumentsView.swift in Sources */, 374DE88028BB896C0062BBF2 /* PlayerDragGesture.swift in Sources */, - 37ECED56289FE166002BC2C9 /* SafeArea.swift in Sources */, 37CEE4BD2677B670005A1EFE /* SingleAssetStream.swift in Sources */, 37C2211D27ADA33300305B41 /* MPVViewController.swift in Sources */, 37A362BE29537AAA00BDF328 /* PlaybackSettings.swift in Sources */, @@ -3094,7 +3092,6 @@ 377FF88F291A99580028EB0B /* HistoryView.swift in Sources */, 3782B94F27553A6700990149 /* SearchSuggestions.swift in Sources */, 378E50FF26FE8EEE00F49626 /* AccountViewButton.swift in Sources */, - 374DE88328BB8A280062BBF2 /* PlayerOrientation.swift in Sources */, 374924F029216C630017D862 /* VideoActions.swift in Sources */, 37169AA62729E2CC0011DE61 /* AccountsBridge.swift in Sources */, 37C7A1DA267CACF50010EAD6 /* TrendingCountry.swift in Sources */, @@ -3176,6 +3173,7 @@ 37FD77002932C4DA00D91A5F /* URL+ByReplacingYatteeProtocol.swift in Sources */, 373CFADB269663F1003CB2C6 /* Thumbnail.swift in Sources */, 3714166F267A8ACC006CA35D /* TrendingView.swift in Sources */, + 37DCD3112A18E8150059A470 /* OrientationModel.swift in Sources */, 3782B9522755667600990149 /* String+Format.swift in Sources */, 37F9619F27BD90BB00058149 /* PlayerBackendType.swift in Sources */, 373CFAEF2697A78B003CB2C6 /* AddToPlaylistView.swift in Sources */, @@ -3246,6 +3244,7 @@ 37A9965A26D6F8CA006E3224 /* HorizontalCells.swift in Sources */, 37EBD8CA27AF26C200F1C24B /* MPVBackend.swift in Sources */, 37635FE4291EA6CF00C11E79 /* AccentButton.swift in Sources */, + 37DCD3152A18F7630059A470 /* SafeAreaModel.swift in Sources */, 37D526DE2720AC4400ED2F5E /* VideosAPI.swift in Sources */, 37484C2526FC83E000287258 /* InstanceForm.swift in Sources */, 37DD9DBD2785D60300539416 /* ScrollViewMatcher.swift in Sources */, @@ -3699,7 +3698,6 @@ 376B2E0926F920D600B1D64D /* SignInRequiredView.swift in Sources */, 378FFBC628660172009E3FBE /* URLParser.swift in Sources */, 37141671267A8ACC006CA35D /* TrendingView.swift in Sources */, - 3732C9FD28C012E600E7DCAF /* SafeArea.swift in Sources */, 37A2B348294723850050933E /* CacheModel.swift in Sources */, 37C3A24727235DA70087A57A /* ChannelPlaylist.swift in Sources */, 3788AC2926F6840700F6BAA9 /* FavoriteItemView.swift in Sources */, diff --git a/iOS/Orientation.swift b/iOS/Orientation.swift index c1560cc9..1f6a0a44 100644 --- a/iOS/Orientation.swift +++ b/iOS/Orientation.swift @@ -31,7 +31,7 @@ struct Orientation { logger.info("rotating to \(orientationString)") if #available(iOS 16, *) { - guard let windowScene = SafeArea.scene else { return } + guard let windowScene = Self.scene else { return } let rotateOrientationMask = rotateOrientation == .portrait ? UIInterfaceOrientationMask.portrait : rotateOrientation == .landscapeLeft ? .landscapeLeft : rotateOrientation == .landscapeRight ? .landscapeRight : @@ -46,4 +46,11 @@ struct Orientation { UINavigationController.attemptRotationToDeviceOrientation() } + + private static var scene: UIWindowScene? { + UIApplication.shared.connectedScenes + .filter { $0.activationState == .foregroundActive } + .compactMap { $0 as? UIWindowScene } + .first + } } diff --git a/Shared/Player/PlayerOrientation.swift b/iOS/OrientationModel.swift similarity index 58% rename from Shared/Player/PlayerOrientation.swift rename to iOS/OrientationModel.swift index 37fce9b1..172d378a 100644 --- a/Shared/Player/PlayerOrientation.swift +++ b/iOS/OrientationModel.swift @@ -1,8 +1,18 @@ import Defaults import Foundation +import Repeat import SwiftUI -extension VideoPlayerView { +final class OrientationModel { + static var shared = OrientationModel() + + var orientation = UIInterfaceOrientation.portrait + var lastOrientation: UIInterfaceOrientation? + var orientationDebouncer = Debouncer(.milliseconds(300)) + internal var orientationObserver: Any? + + private var player = PlayerModel.shared + func configureOrientationUpdatesBasedOnAccelerometer() { let currentOrientation = OrientationTracker.shared.currentInterfaceOrientation if currentOrientation.isLandscape, @@ -15,8 +25,8 @@ extension VideoPlayerView { player.presentingPlayer { DispatchQueue.main.async { - player.controls.presentingControls = false - player.enterFullScreen(showControls: false) + self.player.controls.presentingControls = false + self.player.enterFullScreen(showControls: false) } player.onPresentPlayer.append { @@ -30,42 +40,42 @@ extension VideoPlayerView { queue: .main ) { _ in guard !Defaults[.honorSystemOrientationLock], - player.presentingPlayer, - !player.playingInPictureInPicture, - player.lockedOrientation.isNil + self.player.presentingPlayer, + !self.player.playingInPictureInPicture, + self.player.lockedOrientation.isNil else { return } let orientation = OrientationTracker.shared.currentInterfaceOrientation - guard lastOrientation != orientation else { + guard self.lastOrientation != orientation else { return } - lastOrientation = orientation + self.lastOrientation = orientation DispatchQueue.main.async { guard Defaults[.enterFullscreenInLandscape], - player.presentingPlayer + self.player.presentingPlayer else { return } - orientationDebouncer.callback = { + self.orientationDebouncer.callback = { DispatchQueue.main.async { if orientation.isLandscape { - player.controls.presentingControls = false - player.enterFullScreen(showControls: false) + self.player.controls.presentingControls = false + self.player.enterFullScreen(showControls: false) Orientation.lockOrientation(OrientationTracker.shared.currentInterfaceOrientationMask, andRotateTo: orientation) } else { - player.exitFullScreen(showControls: false) + self.player.exitFullScreen(showControls: false) Orientation.lockOrientation(.allButUpsideDown, andRotateTo: .portrait) } } } - orientationDebouncer.call() + self.orientationDebouncer.call() } } } @@ -74,4 +84,12 @@ extension VideoPlayerView { guard let observer = orientationObserver else { return } NotificationCenter.default.removeObserver(observer) } + + func lockOrientation(_ orientation: UIInterfaceOrientationMask, andRotateTo rotateOrientation: UIInterfaceOrientation? = nil) { + if let rotateOrientation { + self.orientation = rotateOrientation + lastOrientation = rotateOrientation + } + Orientation.lockOrientation(orientation, andRotateTo: rotateOrientation) + } } diff --git a/iOS/SafeAreaModel.swift b/iOS/SafeAreaModel.swift new file mode 100644 index 00000000..95e24d9e --- /dev/null +++ b/iOS/SafeAreaModel.swift @@ -0,0 +1,7 @@ +import Foundation +import SwiftUI + +final class SafeAreaModel: ObservableObject { + static var shared = SafeAreaModel() + @Published var safeArea = EdgeInsets() +}