2022-01-27 14:23:39 +01:00
|
|
|
//
|
|
|
|
// MediaView.swift
|
|
|
|
// MediaView
|
|
|
|
//
|
|
|
|
// Created by Cirno MainasuK on 2021-8-23.
|
|
|
|
// Copyright © 2021 Twidere. All rights reserved.
|
|
|
|
//
|
|
|
|
|
|
|
|
import AVKit
|
|
|
|
import UIKit
|
2022-01-29 12:51:40 +01:00
|
|
|
import Combine
|
2022-02-09 13:35:19 +01:00
|
|
|
import AlamofireImage
|
2022-12-20 17:45:20 +01:00
|
|
|
import SwiftUI
|
2022-12-20 20:22:38 +01:00
|
|
|
import MastodonLocalization
|
2022-12-26 20:07:19 +01:00
|
|
|
import MastodonAsset
|
2022-01-27 14:23:39 +01:00
|
|
|
|
|
|
|
public final class MediaView: UIView {
|
|
|
|
|
2022-01-29 12:51:40 +01:00
|
|
|
var _disposeBag = Set<AnyCancellable>()
|
|
|
|
|
2022-01-27 14:23:39 +01:00
|
|
|
public static let cornerRadius: CGFloat = 0
|
2022-02-18 10:49:20 +01:00
|
|
|
public static let placeholderImage = UIImage.placeholder(color: .systemGray6)
|
2022-01-27 14:23:39 +01:00
|
|
|
|
|
|
|
public let container = TouchBlockingView()
|
|
|
|
|
|
|
|
public private(set) var configuration: Configuration?
|
|
|
|
|
2022-01-29 12:51:40 +01:00
|
|
|
private(set) lazy var blurhashImageView: UIImageView = {
|
|
|
|
let imageView = UIImageView()
|
|
|
|
imageView.contentMode = .scaleAspectFill
|
|
|
|
imageView.isUserInteractionEnabled = false
|
|
|
|
imageView.layer.masksToBounds = true // clip overflow
|
2023-06-26 11:08:18 +02:00
|
|
|
imageView.backgroundColor = .gray
|
|
|
|
imageView.isOpaque = true
|
2022-01-29 12:51:40 +01:00
|
|
|
return imageView
|
|
|
|
}()
|
|
|
|
|
2022-01-27 14:23:39 +01:00
|
|
|
private(set) lazy var imageView: UIImageView = {
|
|
|
|
let imageView = UIImageView()
|
|
|
|
imageView.contentMode = .scaleAspectFill
|
|
|
|
imageView.isUserInteractionEnabled = false
|
|
|
|
imageView.layer.masksToBounds = true // clip overflow
|
|
|
|
return imageView
|
|
|
|
}()
|
|
|
|
|
|
|
|
private(set) lazy var playerViewController: AVPlayerViewController = {
|
|
|
|
let playerViewController = AVPlayerViewController()
|
|
|
|
playerViewController.view.layer.masksToBounds = true
|
|
|
|
playerViewController.view.isUserInteractionEnabled = false
|
2022-02-09 13:35:19 +01:00
|
|
|
playerViewController.videoGravity = .resizeAspectFill
|
|
|
|
playerViewController.updatesNowPlayingInfoCenter = false
|
2022-01-27 14:23:39 +01:00
|
|
|
return playerViewController
|
|
|
|
}()
|
|
|
|
private var playerLooper: AVPlayerLooper?
|
2022-12-26 20:07:19 +01:00
|
|
|
|
2023-06-04 22:15:05 +02:00
|
|
|
let overlayViewController: UIHostingController<InlineMediaOverlayContainer> = {
|
|
|
|
let vc = UIHostingController(rootView: InlineMediaOverlayContainer())
|
2022-12-26 15:29:01 +01:00
|
|
|
vc.view.backgroundColor = .clear
|
|
|
|
return vc
|
2022-12-20 17:45:20 +01:00
|
|
|
}()
|
2022-12-26 15:29:01 +01:00
|
|
|
|
2022-01-27 14:23:39 +01:00
|
|
|
public override init(frame: CGRect) {
|
|
|
|
super.init(frame: frame)
|
|
|
|
_init()
|
|
|
|
}
|
|
|
|
|
|
|
|
public required init?(coder: NSCoder) {
|
|
|
|
super.init(coder: coder)
|
|
|
|
_init()
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
extension MediaView {
|
|
|
|
|
|
|
|
@MainActor
|
|
|
|
public func thumbnail() async -> UIImage? {
|
2022-02-09 13:35:19 +01:00
|
|
|
return imageView.image ?? configuration?.previewImage
|
2022-01-27 14:23:39 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
public func thumbnail() -> UIImage? {
|
2022-02-09 13:35:19 +01:00
|
|
|
return imageView.image ?? configuration?.previewImage
|
2022-01-27 14:23:39 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
extension MediaView {
|
|
|
|
private func _init() {
|
|
|
|
// lazy load content later
|
2022-02-14 12:34:22 +01:00
|
|
|
|
|
|
|
isAccessibilityElement = true
|
2022-01-27 14:23:39 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
public func setup(configuration: Configuration) {
|
|
|
|
self.configuration = configuration
|
|
|
|
|
|
|
|
setupContainerViewHierarchy()
|
|
|
|
|
2022-01-29 12:51:40 +01:00
|
|
|
switch configuration.info {
|
2022-01-27 14:23:39 +01:00
|
|
|
case .image(let info):
|
2022-02-09 13:35:19 +01:00
|
|
|
layoutImage()
|
2023-06-04 22:17:34 +02:00
|
|
|
overlayViewController.rootView.mediaType = .image
|
2022-02-09 13:35:19 +01:00
|
|
|
bindImage(configuration: configuration, info: info)
|
2022-12-22 01:38:05 +01:00
|
|
|
accessibilityHint = L10n.Common.Controls.Status.Media.expandImageHint
|
2022-01-27 14:23:39 +01:00
|
|
|
case .gif(let info):
|
2022-02-09 13:35:19 +01:00
|
|
|
layoutGIF()
|
2023-06-04 22:17:34 +02:00
|
|
|
overlayViewController.rootView.mediaType = .gif
|
2022-02-09 13:35:19 +01:00
|
|
|
bindGIF(configuration: configuration, info: info)
|
2022-12-22 01:38:05 +01:00
|
|
|
accessibilityHint = L10n.Common.Controls.Status.Media.expandGifHint
|
2022-01-27 14:23:39 +01:00
|
|
|
case .video(let info):
|
2022-02-09 13:35:19 +01:00
|
|
|
layoutVideo()
|
2023-06-04 22:17:34 +02:00
|
|
|
overlayViewController.rootView.mediaType = .video
|
2022-02-09 13:35:19 +01:00
|
|
|
bindVideo(configuration: configuration, info: info)
|
2022-12-22 01:38:05 +01:00
|
|
|
accessibilityHint = L10n.Common.Controls.Status.Media.expandVideoHint
|
2022-01-29 12:51:40 +01:00
|
|
|
}
|
2022-02-14 12:34:22 +01:00
|
|
|
|
2022-12-20 19:23:14 +01:00
|
|
|
accessibilityTraits.insert([.button, .image])
|
2022-02-09 13:35:19 +01:00
|
|
|
|
|
|
|
layoutBlurhash()
|
|
|
|
bindBlurhash(configuration: configuration)
|
2022-01-27 14:23:39 +01:00
|
|
|
}
|
|
|
|
|
2022-02-09 13:35:19 +01:00
|
|
|
private func layoutImage() {
|
2022-01-27 14:23:39 +01:00
|
|
|
imageView.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
container.addSubview(imageView)
|
2022-11-17 17:45:27 +01:00
|
|
|
imageView.pinToParent()
|
2022-12-20 17:45:20 +01:00
|
|
|
layoutAlt()
|
2022-02-09 13:35:19 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
private func bindImage(configuration: Configuration, info: Configuration.ImageInfo) {
|
|
|
|
Publishers.CombineLatest3(
|
|
|
|
configuration.$isReveal,
|
|
|
|
configuration.$previewImage,
|
|
|
|
configuration.$blurhashImage
|
2022-01-27 14:23:39 +01:00
|
|
|
)
|
2022-02-09 13:35:19 +01:00
|
|
|
.receive(on: DispatchQueue.main)
|
|
|
|
.sink { [weak self] isReveal, previewImage, blurhashImage in
|
|
|
|
guard let self = self else { return }
|
2022-02-18 10:49:20 +01:00
|
|
|
|
|
|
|
let image = isReveal ?
|
|
|
|
(previewImage ?? blurhashImage ?? MediaView.placeholderImage) :
|
|
|
|
(blurhashImage ?? MediaView.placeholderImage)
|
2022-02-09 13:35:19 +01:00
|
|
|
self.imageView.image = image
|
|
|
|
}
|
|
|
|
.store(in: &configuration.disposeBag)
|
2022-12-20 19:23:14 +01:00
|
|
|
|
2022-12-20 20:22:38 +01:00
|
|
|
bindAlt(configuration: configuration, altDescription: info.altDescription)
|
2022-01-27 14:23:39 +01:00
|
|
|
}
|
2022-12-20 20:22:38 +01:00
|
|
|
|
2022-02-09 13:35:19 +01:00
|
|
|
private func layoutGIF() {
|
2022-01-27 14:23:39 +01:00
|
|
|
// use view controller as View here
|
|
|
|
playerViewController.view.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
container.addSubview(playerViewController.view)
|
2022-11-17 17:45:27 +01:00
|
|
|
playerViewController.view.pinToParent()
|
2023-04-19 22:38:58 +02:00
|
|
|
|
2022-12-20 17:45:20 +01:00
|
|
|
layoutAlt()
|
2022-02-09 13:35:19 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
private func bindGIF(configuration: Configuration, info: Configuration.VideoInfo) {
|
2023-06-04 22:15:05 +02:00
|
|
|
overlayViewController.rootView.mediaDuration = info.durationMS.map { Double($0) / 1000 }
|
|
|
|
overlayViewController.rootView.showDuration = false
|
2023-04-19 22:38:58 +02:00
|
|
|
|
2022-01-27 14:23:39 +01:00
|
|
|
guard let player = setupGIFPlayer(info: info) else { return }
|
|
|
|
setupPlayerLooper(player: player)
|
|
|
|
playerViewController.player = player
|
|
|
|
playerViewController.showsPlaybackControls = false
|
|
|
|
|
|
|
|
// auto play for GIF
|
|
|
|
player.play()
|
2022-12-20 19:23:14 +01:00
|
|
|
|
2022-12-20 20:22:38 +01:00
|
|
|
bindAlt(configuration: configuration, altDescription: info.altDescription)
|
2022-01-27 14:23:39 +01:00
|
|
|
}
|
|
|
|
|
2022-02-09 13:35:19 +01:00
|
|
|
private func layoutVideo() {
|
|
|
|
layoutImage()
|
|
|
|
}
|
|
|
|
|
|
|
|
private func bindVideo(configuration: Configuration, info: Configuration.VideoInfo) {
|
2023-06-04 22:15:05 +02:00
|
|
|
overlayViewController.rootView.mediaDuration = info.durationMS.map { Double($0) / 1000 }
|
|
|
|
overlayViewController.rootView.showDuration = true
|
2023-04-19 22:38:58 +02:00
|
|
|
|
2022-01-27 14:23:39 +01:00
|
|
|
let imageInfo = Configuration.ImageInfo(
|
|
|
|
aspectRadio: info.aspectRadio,
|
2022-12-20 17:03:57 +01:00
|
|
|
assetURL: info.previewURL,
|
|
|
|
altDescription: info.altDescription
|
2022-01-27 14:23:39 +01:00
|
|
|
)
|
2022-02-09 13:35:19 +01:00
|
|
|
bindImage(configuration: configuration, info: imageInfo)
|
2022-01-27 14:23:39 +01:00
|
|
|
}
|
|
|
|
|
2022-12-20 20:22:38 +01:00
|
|
|
private func bindAlt(configuration: Configuration, altDescription: String?) {
|
|
|
|
if configuration.total > 1 {
|
2022-12-22 01:38:05 +01:00
|
|
|
accessibilityLabel = L10n.Common.Controls.Status.Media.accessibilityLabel(
|
2022-12-20 20:22:38 +01:00
|
|
|
altDescription ?? "",
|
|
|
|
configuration.index + 1,
|
|
|
|
configuration.total
|
|
|
|
)
|
|
|
|
} else {
|
|
|
|
accessibilityLabel = altDescription
|
|
|
|
}
|
2022-12-26 15:29:01 +01:00
|
|
|
|
2023-06-04 22:15:05 +02:00
|
|
|
overlayViewController.rootView.altDescription = altDescription
|
2022-12-20 20:22:38 +01:00
|
|
|
}
|
|
|
|
|
2022-02-09 13:35:19 +01:00
|
|
|
private func layoutBlurhash() {
|
2022-01-29 12:51:40 +01:00
|
|
|
blurhashImageView.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
container.addSubview(blurhashImageView)
|
2022-11-17 17:45:27 +01:00
|
|
|
blurhashImageView.pinToParent()
|
2022-02-09 13:35:19 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
private func bindBlurhash(configuration: Configuration) {
|
|
|
|
configuration.$blurhashImage
|
|
|
|
.receive(on: DispatchQueue.main)
|
|
|
|
.assign(to: \.image, on: blurhashImageView)
|
|
|
|
.store(in: &_disposeBag)
|
|
|
|
blurhashImageView.alpha = configuration.isReveal ? 0 : 1
|
2022-01-29 12:51:40 +01:00
|
|
|
|
2022-02-09 13:35:19 +01:00
|
|
|
configuration.$isReveal
|
|
|
|
.dropFirst()
|
|
|
|
.removeDuplicates()
|
|
|
|
.receive(on: DispatchQueue.main)
|
|
|
|
.sink { [weak self] isReveal in
|
|
|
|
guard let self = self else { return }
|
|
|
|
let animator = UIViewPropertyAnimator(duration: 0.3, curve: .easeInOut)
|
|
|
|
animator.addAnimations {
|
|
|
|
self.blurhashImageView.alpha = isReveal ? 0 : 1
|
|
|
|
}
|
|
|
|
animator.startAnimation()
|
|
|
|
}
|
|
|
|
.store(in: &_disposeBag)
|
2022-01-29 12:51:40 +01:00
|
|
|
}
|
|
|
|
|
2022-12-20 17:45:20 +01:00
|
|
|
private func layoutAlt() {
|
2023-06-04 22:15:05 +02:00
|
|
|
overlayViewController.view.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
container.addSubview(overlayViewController.view)
|
|
|
|
overlayViewController.view.pinToParent()
|
2022-12-20 17:45:20 +01:00
|
|
|
}
|
|
|
|
|
2022-01-27 14:23:39 +01:00
|
|
|
public func prepareForReuse() {
|
2022-01-29 12:51:40 +01:00
|
|
|
_disposeBag.removeAll()
|
|
|
|
|
2022-01-27 14:23:39 +01:00
|
|
|
// reset appearance
|
|
|
|
alpha = 1
|
|
|
|
|
|
|
|
// reset image
|
|
|
|
imageView.removeFromSuperview()
|
|
|
|
imageView.removeConstraints(imageView.constraints)
|
|
|
|
imageView.af.cancelImageRequest()
|
|
|
|
imageView.image = nil
|
|
|
|
|
|
|
|
// reset player
|
|
|
|
playerViewController.view.removeFromSuperview()
|
|
|
|
playerViewController.contentOverlayView.flatMap { view in
|
|
|
|
view.removeConstraints(view.constraints)
|
|
|
|
}
|
|
|
|
playerViewController.player?.pause()
|
|
|
|
playerViewController.player = nil
|
|
|
|
playerLooper = nil
|
|
|
|
|
2022-01-29 12:51:40 +01:00
|
|
|
// blurhash
|
|
|
|
blurhashImageView.removeFromSuperview()
|
|
|
|
blurhashImageView.removeConstraints(blurhashImageView.constraints)
|
|
|
|
blurhashImageView.image = nil
|
2023-04-19 22:38:58 +02:00
|
|
|
|
2022-01-27 14:23:39 +01:00
|
|
|
// reset container
|
|
|
|
container.removeFromSuperview()
|
|
|
|
container.removeConstraints(container.constraints)
|
|
|
|
|
2023-06-04 22:15:05 +02:00
|
|
|
overlayViewController.rootView.altDescription = nil
|
|
|
|
overlayViewController.rootView.showDuration = false
|
|
|
|
overlayViewController.rootView.mediaDuration = nil
|
2022-12-26 15:29:01 +01:00
|
|
|
|
2022-01-27 14:23:39 +01:00
|
|
|
// reset configuration
|
|
|
|
configuration = nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
extension MediaView {
|
|
|
|
private func setupGIFPlayer(info: Configuration.VideoInfo) -> AVPlayer? {
|
|
|
|
guard let urlString = info.assetURL,
|
|
|
|
let url = URL(string: urlString)
|
|
|
|
else { return nil }
|
|
|
|
let playerItem = AVPlayerItem(url: url)
|
|
|
|
let player = AVQueuePlayer(playerItem: playerItem)
|
|
|
|
player.isMuted = true
|
|
|
|
return player
|
|
|
|
}
|
|
|
|
|
|
|
|
private func setupPlayerLooper(player: AVPlayer) {
|
|
|
|
guard let queuePlayer = player as? AVQueuePlayer else { return }
|
|
|
|
guard let templateItem = queuePlayer.items().first else { return }
|
|
|
|
playerLooper = AVPlayerLooper(player: queuePlayer, templateItem: templateItem)
|
|
|
|
}
|
|
|
|
|
|
|
|
private func setupContainerViewHierarchy() {
|
|
|
|
guard container.superview == nil else { return }
|
|
|
|
container.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
addSubview(container)
|
2022-11-17 17:45:27 +01:00
|
|
|
container.pinToParent()
|
2022-01-27 14:23:39 +01:00
|
|
|
}
|
|
|
|
}
|