Tusker/Tusker/Screens/Gallery/VideoOverlayViewController....

201 lines
7.1 KiB
Swift

//
// VideoOverlayViewController.swift
// Tusker
//
// Created by Shadowfacts on 3/26/24.
// Copyright © 2024 Shadowfacts. All rights reserved.
//
import UIKit
import AVFoundation
class VideoOverlayViewController: UIViewController {
private static let playImage = UIImage(systemName: "play.fill")!
private static let pauseImage = UIImage(systemName: "pause.fill")!
private let player: AVPlayer
@Box private var playbackSpeed: Float
private var dimmingView: UIView!
private var controlsStack: UIStackView!
private var skipBackButton: VideoOverlayButton!
private var skipForwardButton: VideoOverlayButton!
private var rateObservation: NSKeyValueObservation?
init(player: AVPlayer, playbackSpeed: Box<Float>) {
self.player = player
self._playbackSpeed = playbackSpeed
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
dimmingView = UIView()
dimmingView.backgroundColor = .black
dimmingView.alpha = 0.2
dimmingView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(dimmingView)
NSLayoutConstraint.activate([
dimmingView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
dimmingView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
dimmingView.topAnchor.constraint(equalTo: view.topAnchor),
dimmingView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
skipBackButton = VideoOverlayButton(image: UIImage(systemName: "gobackward.10")!)
skipBackButton.addTarget(self, action: #selector(skipBackPressed), for: .touchUpInside)
let playPauseButton = VideoOverlayButton(image: VideoOverlayViewController.pauseImage)
playPauseButton.addTarget(self, action: #selector(playPausePressed), for: .touchUpInside)
skipForwardButton = VideoOverlayButton(image: UIImage(systemName: "goforward.10")!)
skipForwardButton.addTarget(self, action: #selector(skipForwardPressed), for: .touchUpInside)
controlsStack = UIStackView(arrangedSubviews: [
skipBackButton,
playPauseButton,
skipForwardButton,
])
controlsStack.axis = .horizontal
controlsStack.alignment = .center
controlsStack.spacing = 24
controlsStack.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(controlsStack)
NSLayoutConstraint.activate([
controlsStack.centerXAnchor.constraint(equalTo: view.centerXAnchor),
controlsStack.centerYAnchor.constraint(equalTo: view.centerYAnchor),
skipBackButton.widthAnchor.constraint(equalToConstant: 50),
skipBackButton.heightAnchor.constraint(equalToConstant: 50),
playPauseButton.widthAnchor.constraint(equalToConstant: 66),
playPauseButton.heightAnchor.constraint(equalToConstant: 66),
skipForwardButton.widthAnchor.constraint(equalToConstant: 50),
skipForwardButton.heightAnchor.constraint(equalToConstant: 50),
])
rateObservation = player.observe(\.rate, changeHandler: { player, _ in
MainActor.runUnsafely {
playPauseButton.image = player.rate > 0 ? VideoOverlayViewController.pauseImage : VideoOverlayViewController.playImage
}
})
}
func setVisible(_ visible: Bool) {
loadViewIfNeeded()
view.alpha = visible ? 1 : 0
}
@objc private func playPausePressed() {
if player.rate > 0 {
player.rate = 0
} else {
player.rate = playbackSpeed
}
}
@objc private func skipBackPressed() {
player.seek(to: player.currentTime() - CMTime(value: 10, timescale: 1))
}
@objc private func skipForwardPressed() {
player.seek(to: player.currentTime() + CMTime(value: 10, timescale: 1))
}
}
private class VideoOverlayButton: UIControl {
var image: UIImage? {
get {
imageView.image
}
set {
imageView.image = newValue
}
}
private let backgroundView = UIView()
private let imageView = UIImageView()
private var animator: UIViewPropertyAnimator?
override var isEnabled: Bool {
didSet {
imageView.tintColor = isEnabled ? .white : .lightGray
}
}
init(image: UIImage) {
super.init(frame: .zero)
backgroundView.alpha = 0
backgroundView.backgroundColor = .lightGray.withAlphaComponent(0.5)
backgroundView.translatesAutoresizingMaskIntoConstraints = false
addSubview(backgroundView)
NSLayoutConstraint.activate([
backgroundView.leadingAnchor.constraint(equalTo: leadingAnchor),
backgroundView.trailingAnchor.constraint(equalTo: trailingAnchor),
backgroundView.topAnchor.constraint(equalTo: topAnchor),
backgroundView.bottomAnchor.constraint(equalTo: bottomAnchor),
])
imageView.image = image
imageView.tintColor = .white
imageView.contentMode = .scaleAspectFit
imageView.preferredSymbolConfiguration = .init(scale: .large)
imageView.translatesAutoresizingMaskIntoConstraints = false
addSubview(imageView)
NSLayoutConstraint.activate([
imageView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8),
imageView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8),
imageView.topAnchor.constraint(equalTo: topAnchor, constant: 8),
imageView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -8),
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
backgroundView.layer.cornerRadius = bounds.height / 2
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
bounds.contains(point) ? self : nil
}
override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
UIView.animate(withDuration: 0.2) {
self.backgroundView.alpha = 1
self.backgroundView.transform = CGAffineTransform(scaleX: 1/0.8, y: 1/0.8)
self.transform = CGAffineTransform(scaleX: 0.8, y: 0.8)
}
return super.beginTracking(touch, with: event)
}
override func endTracking(_ touch: UITouch?, with event: UIEvent?) {
UIView.animate(withDuration: 0.2) {
self.backgroundView.alpha = 0
self.backgroundView.transform = .identity
self.transform = .identity
}
}
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
if gestureRecognizer is UITapGestureRecognizer {
return false
}
return true
}
}