jellyflood/Swiftfin tvOS/Views/VideoPlayerContainerState/PlaybackControls/VideoPlayerContainerView.swift

278 lines
10 KiB
Swift

//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2025 Jellyfin & Jellyfin Contributors
//
import Combine
import Engine
import SwiftUI
extension VideoPlayer {
struct VideoPlayerContainerView<Player: View, PlaybackControls: View>: UIViewControllerRepresentable {
private let containerState: VideoPlayerContainerState
private let manager: MediaPlayerManager
private let player: () -> Player
private let playbackControls: () -> PlaybackControls
init(
containerState: VideoPlayerContainerState,
manager: MediaPlayerManager,
@ViewBuilder player: @escaping () -> Player,
@ViewBuilder playbackControls: @escaping () -> PlaybackControls
) {
self.containerState = containerState
self.manager = manager
self.player = player
self.playbackControls = playbackControls
}
func makeUIViewController(context: Context) -> UIVideoPlayerContainerViewController {
UIVideoPlayerContainerViewController(
containerState: containerState,
manager: manager,
player: player().eraseToAnyView(),
playbackControls: playbackControls().eraseToAnyView()
)
}
func updateUIViewController(
_ uiViewController: UIVideoPlayerContainerViewController,
context: Context
) {}
}
// MARK: - UIVideoPlayerContainerViewController
class UIVideoPlayerContainerViewController: UIViewController {
private struct PlayerContainerView: View {
@EnvironmentObject
private var containerState: VideoPlayerContainerState
let player: AnyView
var body: some View {
player
.overlay(Color.black.opacity(containerState.isPresentingPlaybackControls ? 0.3 : 0.0))
.animation(.linear(duration: 0.2), value: containerState.isPresentingPlaybackControls)
}
}
private lazy var playerViewController: HostingController<AnyView> = {
let controller = HostingController(
content: PlayerContainerView(player: player)
.environmentObject(containerState)
.environmentObject(manager)
.eraseToAnyView()
)
controller.disablesSafeArea = true
controller.automaticallyAllowUIKitAnimationsForNextUpdate = true
controller.view.translatesAutoresizingMaskIntoConstraints = false
return controller
}()
private lazy var playbackControlsViewController: HostingController<AnyView> = {
let controller = HostingController(
content: playbackControls
.environment(\.onPressEventPublisher, onPressEvent)
.environmentObject(containerState)
.environmentObject(containerState.scrubbedSeconds)
.environmentObject(focusGuide)
.environmentObject(manager)
.eraseToAnyView()
)
controller.disablesSafeArea = true
controller.automaticallyAllowUIKitAnimationsForNextUpdate = true
controller.view.translatesAutoresizingMaskIntoConstraints = false
return controller
}()
private lazy var supplementContainerViewController: HostingController<AnyView> = {
let content = SupplementContainerView()
.environmentObject(containerState)
.environmentObject(focusGuide)
.environmentObject(manager)
.eraseToAnyView()
let controller = HostingController(content: content)
controller.disablesSafeArea = true
controller.automaticallyAllowUIKitAnimationsForNextUpdate = true
controller.view.translatesAutoresizingMaskIntoConstraints = false
return controller
}()
private var playerView: UIView { playerViewController.view }
private var playbackControlsView: UIView { playbackControlsViewController.view }
private var supplementContainerView: UIView { supplementContainerViewController.view }
private var supplementRegularConstraints: [NSLayoutConstraint] = []
private var playerRegularConstraints: [NSLayoutConstraint] = []
private var playbackControlsConstraints: [NSLayoutConstraint] = []
private var supplementHeightAnchor: NSLayoutConstraint!
private var supplementBottomAnchor: NSLayoutConstraint!
private let manager: MediaPlayerManager
private let player: AnyView
private let playbackControls: AnyView
private let containerState: VideoPlayerContainerState
let focusGuide = FocusGuide()
let onPressEvent = OnPressEvent()
private var cancellables: Set<AnyCancellable> = []
init(
containerState: VideoPlayerContainerState,
manager: MediaPlayerManager,
player: AnyView,
playbackControls: AnyView
) {
self.containerState = containerState
self.manager = manager
self.player = player
self.playbackControls = playbackControls
super.init(nibName: nil, bundle: nil)
containerState.containerView = self
containerState.manager = manager
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - didPresent
func presentSupplementContainer(
_ didPresent: Bool
) {
if didPresent {
self.supplementBottomAnchor.constant = -(500 + EdgeInsets.edgePadding * 2)
} else {
self.supplementBottomAnchor.constant = -(100 + EdgeInsets.edgePadding)
}
containerState.isPresentingPlaybackControls = !didPresent
containerState.supplementOffset = supplementBottomAnchor.constant
UIView.animate(
withDuration: 0.75,
delay: 0,
usingSpringWithDamping: 0.8,
initialSpringVelocity: 0.4,
options: .allowUserInteraction
) {
self.view.layoutIfNeeded()
}
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .black
setupViews()
setupConstraints()
let gesture = UITapGestureRecognizer(target: self, action: #selector(ignorePress))
gesture.allowedPressTypes = [NSNumber(value: UIPress.PressType.menu.rawValue)]
view.addGestureRecognizer(gesture)
}
private func setupViews() {
addChild(playerViewController)
view.addSubview(playerView)
playerViewController.didMove(toParent: self)
playerView.backgroundColor = .black
addChild(playbackControlsViewController)
view.addSubview(playbackControlsView)
playbackControlsViewController.didMove(toParent: self)
playbackControlsView.backgroundColor = .clear
addChild(supplementContainerViewController)
view.addSubview(supplementContainerView)
supplementContainerViewController.didMove(toParent: self)
supplementContainerView.backgroundColor = .clear
}
private func setupConstraints() {
playerRegularConstraints = [
playerView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
playerView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
playerView.topAnchor.constraint(equalTo: view.topAnchor),
playerView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
]
NSLayoutConstraint.activate(playerRegularConstraints)
supplementBottomAnchor = supplementContainerView.topAnchor.constraint(
equalTo: view.bottomAnchor,
constant: -(100 + EdgeInsets.edgePadding)
)
containerState.supplementOffset = supplementBottomAnchor.constant
let constant = (500 + EdgeInsets.edgePadding * 2)
supplementHeightAnchor = supplementContainerView.heightAnchor.constraint(equalToConstant: constant)
supplementRegularConstraints = [
supplementContainerView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
supplementContainerView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
supplementBottomAnchor,
supplementHeightAnchor,
]
NSLayoutConstraint.activate(supplementRegularConstraints)
playbackControlsConstraints = [
playbackControlsView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
playbackControlsView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
playbackControlsView.topAnchor.constraint(equalTo: view.topAnchor),
playbackControlsView.bottomAnchor.constraint(equalTo: supplementContainerView.topAnchor),
]
NSLayoutConstraint.activate(playbackControlsConstraints)
}
@objc
func ignorePress() {}
override func pressesBegan(_ presses: Set<UIPress>, with event: UIPressesEvent?) {
print(presses)
guard let buttonPress = presses.first else { return }
onPressEvent.send((type: buttonPress.type, phase: buttonPress.phase))
}
}
}
extension VideoPlayer.UIVideoPlayerContainerViewController {
typealias PressEvent = (type: UIPress.PressType, phase: UIPress.Phase)
typealias OnPressEvent = LegacyEventPublisher<PressEvent>
}
@propertyWrapper
struct OnPressEvent: DynamicProperty {
@Environment(\.onPressEventPublisher)
private var publisher
var wrappedValue: VideoPlayer.UIVideoPlayerContainerViewController.OnPressEvent {
publisher
}
}
extension EnvironmentValues {
@Entry
var onPressEventPublisher: VideoPlayer.UIVideoPlayerContainerViewController.OnPressEvent = .init()
}