Player overlaying other views and swipe gesture (fix #44, #130)

This commit is contained in:
Arkadiusz Fal 2022-05-28 23:41:23 +02:00
parent 687949fbd5
commit 78d7693128
17 changed files with 187 additions and 209 deletions

View File

@ -1,5 +1,6 @@
import AVFAudio
import CoreMedia
import Defaults
import Foundation
import Logging
import SwiftUI
@ -247,7 +248,13 @@ final class MPVBackend: PlayerBackend {
client?.stop()
}
func enterFullScreen() {}
func enterFullScreen() {
model.toggleFullscreen(controls?.playingFullscreen ?? false)
if Defaults[.lockLandscapeWhenEnteringFullscreen] {
Orientation.lockOrientation(.landscape, andRotateTo: UIDevice.current.orientation.isLandscape ? nil : .landscapeRight)
}
}
func exitFullScreen() {}

View File

@ -134,11 +134,11 @@ final class MPVClient: ObservableObject {
}
var currentTime: CMTime {
CMTime.secondsInDefaultTimescale(getDouble("time-pos"))
CMTime.secondsInDefaultTimescale(mpv.isNil ? -1 : getDouble("time-pos"))
}
var duration: CMTime {
CMTime.secondsInDefaultTimescale(getDouble("duration"))
CMTime.secondsInDefaultTimescale(mpv.isNil ? -1 : getDouble("duration"))
}
func seek(relative time: CMTime, completionHandler: ((Bool) -> Void)? = nil) {

View File

@ -57,8 +57,6 @@ final class PlayerModel: ObservableObject {
@Published var preservedTime: CMTime?
@Published var playerNavigationLinkActive = false { didSet { handleNavigationViewPlayerPresentationChange() } }
@Published var sponsorBlock = SponsorBlockAPI()
@Published var segmentRestorationTime: CMTime?
@Published var lastSkipped: Segment? { didSet { rebuildTVMenu() } }
@ -120,23 +118,24 @@ final class PlayerModel: ObservableObject {
}
func show() {
guard !presentingPlayer else {
#if os(macOS)
#if os(macOS)
if presentingPlayer {
Windows.player.focus()
#endif
return
}
return
}
#endif
presentingPlayer = true
#if os(macOS)
Windows.player.open()
Windows.player.focus()
#endif
presentingPlayer = true
}
func hide() {
controls.playingFullscreen = false
presentingPlayer = false
playerNavigationLinkActive = false
#if os(iOS)
if Defaults[.lockPortraitWhenBrowsing] {
@ -206,18 +205,25 @@ final class PlayerModel: ObservableObject {
backend.pause()
}
func play(_ video: Video, at time: CMTime? = nil, inNavigationView: Bool = false) {
playNow(video, at: time)
func play(_ video: Video, at time: CMTime? = nil) {
var delay = 0.0
#if !os(macOS)
delay = 0.3
#endif
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
guard let self = self else {
return
}
self.playNow(video, at: time)
}
guard !playingInPictureInPicture else {
return
}
if inNavigationView {
playerNavigationLinkActive = true
} else {
show()
}
show()
}
func playStream(
@ -297,7 +303,18 @@ final class PlayerModel: ObservableObject {
}
private func handlePresentationChange() {
backend.setNeedsDrawing(presentingPlayer)
var delay = 0.0
#if os(iOS)
if presentingPlayer {
delay = 0.2
}
#endif
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
self?.backend.setNeedsDrawing(self?.presentingPlayer ?? false)
}
controls.hide()
#if !os(macOS)
@ -323,17 +340,6 @@ final class PlayerModel: ObservableObject {
}
}
private func handleNavigationViewPlayerPresentationChange() {
backend.setNeedsDrawing(playerNavigationLinkActive)
controls.hide()
if pauseOnHidingPlayer, !playingInPictureInPicture, !playerNavigationLinkActive {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.pause()
}
}
}
func changeActiveBackend(from: PlayerBackendType, to: PlayerBackendType) {
Defaults[.activeBackend] = to
self.activeBackend = to

View File

@ -8,7 +8,7 @@ extension PlayerModel {
currentItem?.video
}
func play(_ videos: [Video], shuffling: Bool = false, inNavigationView: Bool = false) {
func play(_ videos: [Video], shuffling: Bool = false) {
let videosToPlay = shuffling ? videos.shuffled() : videos
guard let first = videosToPlay.first else {
@ -27,11 +27,7 @@ extension PlayerModel {
}
}
if inNavigationView {
playerNavigationLinkActive = true
} else {
show()
}
show()
}
func playNext(_ video: Video) {

View File

@ -1,10 +1,6 @@
import Foundation
import SwiftUI
private struct InNavigationViewKey: EnvironmentKey {
static let defaultValue = false
}
private struct InChannelViewKey: EnvironmentKey {
static let defaultValue = false
}
@ -40,11 +36,6 @@ private struct ScrollViewBottomPaddingKey: EnvironmentKey {
}
extension EnvironmentValues {
var inNavigationView: Bool {
get { self[InNavigationViewKey.self] }
set { self[InNavigationViewKey.self] = newValue }
}
var inChannelView: Bool {
get { self[InChannelViewKey.self] }
set { self[InChannelViewKey.self] = newValue }

View File

@ -63,24 +63,6 @@ struct AppSidebarNavigation: View {
}
}
.environment(\.navigationStyle, .sidebar)
#if os(iOS)
.background(
EmptyView().fullScreenCover(isPresented: $player.presentingPlayer) {
VideoPlayerView()
.environmentObject(accounts)
.environmentObject(comments)
.environmentObject(instances)
.environmentObject(navigation)
.environmentObject(player)
.environmentObject(playerControls)
.environmentObject(playlists)
.environmentObject(recents)
.environmentObject(subscriptions)
.environmentObject(thumbnailsModel)
.environment(\.navigationStyle, .sidebar)
}
)
#endif
}
var toolbarContent: some ToolbarContent {

View File

@ -51,14 +51,11 @@ struct AppTabNavigation: View {
ChannelVideosView(channel: channel)
.environment(\.managedObjectContext, persistenceController.container.viewContext)
.environment(\.inChannelView, true)
.environment(\.inNavigationView, true)
.environmentObject(accounts)
.environmentObject(navigation)
.environmentObject(player)
.environmentObject(subscriptions)
.environmentObject(thumbnailsModel)
.background(playerNavigationLink)
}
}
}
@ -69,25 +66,15 @@ struct AppTabNavigation: View {
NavigationView {
ChannelPlaylistView(playlist: playlist)
.environment(\.managedObjectContext, persistenceController.container.viewContext)
.environment(\.inNavigationView, true)
.environmentObject(accounts)
.environmentObject(navigation)
.environmentObject(player)
.environmentObject(subscriptions)
.environmentObject(thumbnailsModel)
.background(playerNavigationLink)
}
}
}
)
.background(
EmptyView().fullScreenCover(isPresented: $player.presentingPlayer) {
videoPlayer
.environment(\.managedObjectContext, persistenceController.container.viewContext)
.environment(\.navigationStyle, .tab)
}
)
}
private var favoritesNavigationView: some View {
@ -172,15 +159,6 @@ struct AppTabNavigation: View {
.tag(TabSelection.search)
}
private var playerNavigationLink: some View {
NavigationLink(isActive: $player.playerNavigationLinkActive, destination: {
videoPlayer
.environment(\.inNavigationView, true)
}) {
EmptyView()
}
}
private var videoPlayer: some View {
VideoPlayerView()
.environmentObject(accounts)

View File

@ -57,50 +57,54 @@ struct ContentView: View {
.environmentObject(subscriptions)
.environmentObject(thumbnailsModel)
// iOS 14 has problem with multiple sheets in one view
// but it's ok when it's in background
.background(
EmptyView().sheet(isPresented: $navigation.presentingWelcomeScreen) {
WelcomeScreen()
.environmentObject(accounts)
.environmentObject(navigation)
}
)
#if !os(tvOS)
.onOpenURL { OpenURLHandler(accounts: accounts, player: player).handle($0) }
.background(
EmptyView().sheet(isPresented: $navigation.presentingAddToPlaylist) {
AddToPlaylistView(video: navigation.videoToAddToPlaylist)
.environmentObject(playlists)
}
)
.background(
EmptyView().sheet(isPresented: $navigation.presentingPlaylistForm) {
PlaylistFormView(playlist: $navigation.editedPlaylist)
.environmentObject(accounts)
.environmentObject(playlists)
}
)
.background(
EmptyView().sheet(isPresented: $navigation.presentingSettings, onDismiss: openWelcomeScreenIfAccountEmpty) {
SettingsView()
.environmentObject(accounts)
.environmentObject(instances)
.environmentObject(player)
}
)
#if !os(macOS)
.overlay(videoPlayer)
#endif
.alert(isPresented: $navigation.presentingUnsubscribeAlert) {
Alert(
title: Text(
"Are you sure you want to unsubscribe from \(navigation.channelToUnsubscribe.name)?"
),
primaryButton: .destructive(Text("Unsubscribe")) {
subscriptions.unsubscribe(navigation.channelToUnsubscribe.id)
},
secondaryButton: .cancel()
// iOS 14 has problem with multiple sheets in one view
// but it's ok when it's in background
.background(
EmptyView().sheet(isPresented: $navigation.presentingWelcomeScreen) {
WelcomeScreen()
.environmentObject(accounts)
.environmentObject(navigation)
}
)
}
#if !os(tvOS)
.onOpenURL { OpenURLHandler(accounts: accounts, player: player).handle($0) }
.background(
EmptyView().sheet(isPresented: $navigation.presentingAddToPlaylist) {
AddToPlaylistView(video: navigation.videoToAddToPlaylist)
.environmentObject(playlists)
}
)
.background(
EmptyView().sheet(isPresented: $navigation.presentingPlaylistForm) {
PlaylistFormView(playlist: $navigation.editedPlaylist)
.environmentObject(accounts)
.environmentObject(playlists)
}
)
.background(
EmptyView().sheet(isPresented: $navigation.presentingSettings, onDismiss: openWelcomeScreenIfAccountEmpty) {
SettingsView()
.environmentObject(accounts)
.environmentObject(instances)
.environmentObject(player)
}
)
#endif
.alert(isPresented: $navigation.presentingUnsubscribeAlert) {
Alert(
title: Text(
"Are you sure you want to unsubscribe from \(navigation.channelToUnsubscribe.name)?"
),
primaryButton: .destructive(Text("Unsubscribe")) {
subscriptions.unsubscribe(navigation.channelToUnsubscribe.id)
},
secondaryButton: .cancel()
)
}
}
func configure() {
@ -222,6 +226,21 @@ struct ContentView: View {
navigation.presentingWelcomeScreen = true
}
var videoPlayer: some View {
VideoPlayerView()
.environmentObject(accounts)
.environmentObject(comments)
.environmentObject(instances)
.environmentObject(navigation)
.environmentObject(player)
.environmentObject(playerControls)
.environmentObject(playlists)
.environmentObject(recents)
.environmentObject(subscriptions)
.environmentObject(thumbnailsModel)
.environment(\.navigationStyle, .sidebar)
}
}
struct ContentView_Previews: PreviewProvider {

View File

@ -178,11 +178,7 @@ extension AppleAVPlayerViewController: AVPlayerViewControllerDelegate {
restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void
) {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
if self.navigationModel.presentingChannel {
self.playerModel.playerNavigationLinkActive = true
} else {
self.playerModel.show()
}
self.playerModel.show()
#if os(tvOS)
if self.playerModel.playingInPictureInPicture {
@ -198,7 +194,6 @@ extension AppleAVPlayerViewController: AVPlayerViewControllerDelegate {
func playerViewControllerWillStartPictureInPicture(_: AVPlayerViewController) {
playerModel.playingInPictureInPicture = true
playerModel.playerNavigationLinkActive = false
}
func playerViewControllerWillStopPictureInPicture(_: AVPlayerViewController) {

View File

@ -240,7 +240,7 @@ struct PlayerControls: View {
player.hide()
player.closePiP()
var delay = 0.3
var delay = 0.2
#if os(macOS)
delay = 0.0
#endif

View File

@ -21,7 +21,6 @@ struct VideoDetails: View {
@State private var currentPage = Page.info
@Environment(\.presentationMode) private var presentationMode
@Environment(\.inNavigationView) private var inNavigationView
@Environment(\.navigationStyle) private var navigationStyle
@EnvironmentObject<AccountsModel> private var accounts
@ -112,7 +111,6 @@ struct VideoDetails: View {
.edgesIgnoringSafeArea(.horizontal)
}
}
.padding(.top, inNavigationView && fullScreen ? 10 : 0)
.onAppear {
if video.isNil && !sidebarQueue {
currentPage = .queue

View File

@ -37,6 +37,10 @@ struct VideoPlayerView: View {
var mouseLocation: CGPoint { NSEvent.mouseLocation }
#endif
#if !os(macOS)
@State private var playerOffset = UIScreen.main.bounds.height
#endif
@EnvironmentObject<AccountsModel> private var accounts
@EnvironmentObject<PlayerControlsModel> private var playerControls
@EnvironmentObject<PlayerModel> private var player
@ -54,10 +58,6 @@ struct VideoPlayerView: View {
content
.onAppear {
playerSize = geometry.size
#if os(iOS)
configureOrientationUpdatesBasedOnAccelerometer()
#endif
}
}
.onChange(of: geometry.size) { size in
@ -70,22 +70,10 @@ struct VideoPlayerView: View {
.onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in
handleOrientationDidChangeNotification()
}
.onDisappear {
guard !playerControls.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
}
.offset(y: playerOffset)
.animation(.easeIn(duration: 0.2), value: playerOffset)
#endif
}
@ -138,6 +126,59 @@ struct VideoPlayerView: View {
hoveringPlayer = hovering
hovering ? playerControls.show() : playerControls.hide()
}
#if !os(tvOS)
.onChange(of: player.presentingPlayer) { newValue in
if newValue {
playerOffset = 0
#if os(iOS)
configureOrientationUpdatesBasedOnAccelerometer()
#endif
} else {
#if !os(macOS)
playerOffset = UIScreen.main.bounds.height
#if os(iOS)
if Defaults[.lockPortraitWhenBrowsing] {
Orientation.lockOrientation(.portrait, andRotateTo: .portrait)
} else {
Orientation.lockOrientation(.allButUpsideDown)
}
motionManager?.stopAccelerometerUpdates()
motionManager = nil
#endif
#endif
}
}
.gesture(
DragGesture(minimumDistance: 0)
.onChanged { value in
guard !fullScreenLayout else {
return
}
player.backend.setNeedsDrawing(false)
let drag = value.translation.height
guard drag > 0 else {
return
}
withAnimation(.easeIn(duration: 0.2)) {
playerOffset = drag
}
}
.onEnded { _ in
if playerOffset > 100 {
player.backend.setNeedsDrawing(true)
player.hide()
} else {
player.show()
playerOffset = 0
}
}
)
#endif
#if os(macOS)
.onAppear(perform: {
NSEvent.addLocalMonitorForEvents(matching: [.mouseMoved]) {
@ -406,7 +447,8 @@ struct VideoPlayerView: View {
} else {
guard abs(acceleration.z) <= 0.74,
player.lockedOrientation.isNil,
enterFullscreenInLandscape
enterFullscreenInLandscape,
!lockLandscapeOnRotation
else {
return
}
@ -421,6 +463,7 @@ struct VideoPlayerView: View {
}
private func handleOrientationDidChangeNotification() {
playerOffset = playerOffset == 0 ? 0 : UIScreen.main.bounds.height
let newOrientation = UIApplication.shared.windows.first?.windowScene?.interfaceOrientation
if newOrientation?.isLandscape ?? false,
player.presentingPlayer,

View File

@ -6,7 +6,6 @@ import SwiftUI
struct VideoCell: View {
private var video: Video
@Environment(\.inNavigationView) private var inNavigationView
@Environment(\.navigationStyle) private var navigationStyle
#if os(iOS)
@ -46,11 +45,8 @@ struct VideoCell: View {
.buttonStyle(.plain)
.contentShape(RoundedRectangle(cornerRadius: thumbnailRoundingCornerRadius))
.contextMenu {
VideoContextMenuView(
video: video,
playerNavigationLinkActive: $player.playerNavigationLinkActive
)
.environmentObject(accounts)
VideoContextMenuView(video: video)
.environmentObject(accounts)
}
}
@ -93,7 +89,7 @@ struct VideoCell: View {
player.avPlayerBackend.startPictureInPictureOnPlay = player.playingInPictureInPicture
player.play(video, at: playAt, inNavigationView: inNavigationView)
player.play(video, at: playAt)
}
}

View File

@ -10,7 +10,6 @@ struct ChannelPlaylistView: View {
@StateObject private var store = Store<ChannelPlaylist>()
@Environment(\.colorScheme) private var colorScheme
@Environment(\.inNavigationView) private var inNavigationView
@EnvironmentObject<AccountsModel> private var accounts
@EnvironmentObject<PlayerModel> private var player
@ -24,19 +23,9 @@ struct ChannelPlaylistView: View {
}
var body: some View {
#if os(iOS)
if inNavigationView {
content
} else {
BrowserPlayerControls {
content
}
}
#else
BrowserPlayerControls {
content
}
#endif
BrowserPlayerControls {
content
}
}
var content: some View {
@ -94,9 +83,6 @@ struct ChannelPlaylistView: View {
}
}
.navigationTitle(playlist.title)
#if os(iOS)
.navigationBarHidden(player.playerNavigationLinkActive)
#endif
#endif
}
@ -110,7 +96,7 @@ struct ChannelPlaylistView: View {
private var playButton: some View {
Button {
player.play(videos, inNavigationView: inNavigationView)
player.play(videos)
} label: {
Label("Play All", systemImage: "play")
}
@ -118,7 +104,7 @@ struct ChannelPlaylistView: View {
private var shuffleButton: some View {
Button {
player.play(videos, shuffling: true, inNavigationView: inNavigationView)
player.play(videos, shuffling: true)
} label: {
Label("Shuffle", systemImage: "shuffle")
}

View File

@ -13,7 +13,6 @@ struct ChannelVideosView: View {
@Environment(\.colorScheme) private var colorScheme
#if os(iOS)
@Environment(\.inNavigationView) private var inNavigationView
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
@EnvironmentObject<PlayerModel> private var player
#endif
@ -29,19 +28,9 @@ struct ChannelVideosView: View {
}
var body: some View {
#if os(iOS)
if inNavigationView {
content
} else {
BrowserPlayerControls {
content
}
}
#else
BrowserPlayerControls {
content
}
#endif
BrowserPlayerControls {
content
}
}
var content: some View {
@ -115,9 +104,6 @@ struct ChannelVideosView: View {
resource.load()
}
}
#if os(iOS)
.navigationBarHidden(player.playerNavigationLinkActive)
#endif
.navigationTitle(navigationTitle)
return Group {

View File

@ -4,7 +4,6 @@ import SwiftUI
struct PlaylistVideosView: View {
let playlist: Playlist
@Environment(\.inNavigationView) private var inNavigationView
@EnvironmentObject<PlayerModel> private var player
@EnvironmentObject<PlaylistsModel> private var model
@ -66,13 +65,13 @@ struct PlaylistVideosView: View {
FavoriteButton(item: FavoriteItem(section: .channelPlaylist(playlist.id, playlist.title)))
Button {
player.play(videos, inNavigationView: inNavigationView)
player.play(videos)
} label: {
Label("Play All", systemImage: "play")
}
Button {
player.play(videos, shuffling: true, inNavigationView: inNavigationView)
player.play(videos, shuffling: true)
} label: {
Label("Shuffle", systemImage: "shuffle")
}

View File

@ -5,9 +5,6 @@ import SwiftUI
struct VideoContextMenuView: View {
let video: Video
@Binding var playerNavigationLinkActive: Bool
@Environment(\.inNavigationView) private var inNavigationView
@Environment(\.inChannelView) private var inChannelView
@Environment(\.inChannelPlaylistView) private var inChannelPlaylistView
@Environment(\.navigationStyle) private var navigationStyle
@ -26,9 +23,8 @@ struct VideoContextMenuView: View {
private var viewContext: NSManagedObjectContext = PersistenceController.shared.container.viewContext
init(video: Video, playerNavigationLinkActive: Binding<Bool>) {
init(video: Video) {
self.video = video
_playerNavigationLinkActive = playerNavigationLinkActive
_watchRequest = video.watchFetchRequest
}
@ -111,7 +107,7 @@ struct VideoContextMenuView: View {
private var continueButton: some View {
Button {
player.play(video, at: .secondsInDefaultTimescale(watch!.stoppedAt), inNavigationView: inNavigationView)
player.play(video, at: .secondsInDefaultTimescale(watch!.stoppedAt))
} label: {
Label("Continue from \(watch!.stoppedAt.formattedAsPlaybackTime() ?? "where I left off")", systemImage: "playpause")
}
@ -131,7 +127,7 @@ struct VideoContextMenuView: View {
private var playNowButton: some View {
Button {
player.play(video, inNavigationView: inNavigationView)
player.play(video)
} label: {
Label("Play Now", systemImage: "play")
}