Tusker/Tusker/Views/Attachments/AttachmentView.swift

346 lines
12 KiB
Swift

//
// AttachmentView.swift
// Tusker
//
// Created by Shadowfacts on 8/31/18.
// Copyright © 2018 Shadowfacts. All rights reserved.
//
import UIKit
import Pachyderm
import AVFoundation
protocol AttachmentViewDelegate: AnyObject {
func attachmentViewGallery(startingAt index: Int) -> GalleryViewController?
func attachmentViewPresent(_ vc: UIViewController, animated: Bool)
}
class AttachmentView: GIFImageView {
static let queue = DispatchQueue(label: "Attachment Thumbnail", qos: .userInitiated, attributes: .concurrent)
weak var delegate: AttachmentViewDelegate?
var playImageView: UIImageView?
var gifvView: GifvAttachmentView?
var attachment: Attachment!
var index: Int!
var expectedSize: CGSize!
private var attachmentRequest: ImageCache.Request?
private var source: Source?
private var autoplayGifs: Bool {
Preferences.shared.automaticallyPlayGifs && !ProcessInfo.processInfo.isLowPowerModeEnabled
}
private var isGrayscale = false
init(attachment: Attachment, index: Int, expectedSize: CGSize) {
super.init(image: nil)
commonInit()
self.attachment = attachment
self.index = index
self.expectedSize = expectedSize
loadAttachment()
}
deinit {
attachmentRequest?.cancel()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
private func commonInit() {
contentMode = .scaleAspectFill
layer.masksToBounds = true
isUserInteractionEnabled = true
addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(imagePressed)))
NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(gifPlaybackModeChanged), name: .NSProcessInfoPowerStateDidChange, object: nil)
addInteraction(UIContextMenuInteraction(delegate: self))
isAccessibilityElement = true
accessibilityTraits = [.image, .button]
}
@objc private func preferencesChanged() {
gifPlaybackModeChanged()
if isGrayscale != Preferences.shared.grayscaleImages {
ImageGrayscalifier.queue.async {
self.displayImage()
}
}
}
@objc private func gifPlaybackModeChanged() {
// NSProcessInfoPowerStateDidChange is sometimes fired on a background thread
DispatchQueue.main.async {
if self.attachment.kind == .image,
let gifController = self.gifController {
if self.autoplayGifs && !self.isAnimatingGIF {
gifController.attach(to: self)
gifController.startAnimating()
} else if !self.autoplayGifs && self.isAnimatingGIF {
// detach instead of stopping so that any other attached gif views keep animating
self.detachGIFController()
}
} else if self.attachment.kind == .gifv,
let gifvView = self.gifvView {
if self.autoplayGifs {
gifvView.player.play()
} else {
gifvView.player.pause()
}
}
}
}
func loadAttachment() {
guard AttachmentsContainerView.supportedAttachmentTypes.contains(attachment.kind) else {
preconditionFailure("invalid attachment type")
}
if let hash = attachment.blurHash {
AttachmentView.queue.async { [weak self] in
guard let self = self else { return }
let size: CGSize
if let meta = self.attachment.meta,
let width = meta.width, let height = meta.height {
size = CGSize(width: width, height: height)
} else if let orig = self.attachment.meta?.original,
let width = orig.width, let height = orig.height {
size = CGSize(width: width, height: height)
} else {
size = self.expectedSize
}
guard var preview = UIImage(blurHash: hash, size: size) else {
return
}
if Preferences.shared.grayscaleImages,
let grayscale = ImageGrayscalifier.convert(url: nil, cgImage: preview.cgImage!) {
preview = grayscale
}
DispatchQueue.main.async { [weak self] in
guard let self = self, self.image == nil else { return }
self.image = preview
}
}
}
switch attachment.kind {
case .image:
loadImage()
case .video:
loadVideo()
case .audio:
loadAudio()
case .gifv:
loadGifv()
default:
preconditionFailure("invalid attachment type")
}
}
func loadImage() {
let attachmentURL = attachment.url
attachmentRequest = ImageCache.attachments.get(attachmentURL) { [weak self] (data, _) in
guard let self = self, let data = data else { return }
DispatchQueue.main.async {
self.attachmentRequest = nil
}
if self.attachment.url.pathExtension == "gif" {
self.source = .gifData(attachmentURL, data)
let controller = GIFController(gifData: data)
DispatchQueue.main.async {
controller.attach(to: self)
if self.autoplayGifs {
controller.startAnimating()
}
}
if !self.autoplayGifs {
self.displayImage()
}
} else {
self.source = .imageData(attachmentURL, data)
self.displayImage()
}
}
}
func loadVideo() {
if let previewURL = self.attachment.previewURL {
attachmentRequest = ImageCache.attachments.get(previewURL, completion: { [weak self] (data, _ )in
guard let self = self, let data = data else { return }
DispatchQueue.main.async {
self.attachmentRequest = nil
self.source = .imageData(previewURL, data)
self.displayImage()
}
})
} else {
let attachmentURL = self.attachment.url
AttachmentView.queue.async {
let asset = AVURLAsset(url: attachmentURL)
let generator = AVAssetImageGenerator(asset: asset)
generator.appliesPreferredTrackTransform = true
guard let image = try? generator.copyCGImage(at: .zero, actualTime: nil) else { return }
self.source = .cgImage(attachmentURL, image)
self.displayImage()
}
}
let playImageView = UIImageView(image: UIImage(systemName: "play.circle.fill"))
playImageView.translatesAutoresizingMaskIntoConstraints = false
addSubview(playImageView)
NSLayoutConstraint.activate([
playImageView.widthAnchor.constraint(equalToConstant: 50),
playImageView.heightAnchor.constraint(equalToConstant: 50),
playImageView.centerXAnchor.constraint(equalTo: centerXAnchor),
playImageView.centerYAnchor.constraint(equalTo: centerYAnchor),
])
}
func loadAudio() {
let label = UILabel()
label.text = "Audio Only"
let playImageView = UIImageView(image: UIImage(systemName: "play.circle.fill"))
let stack = UIStackView(arrangedSubviews: [
label,
playImageView
])
stack.translatesAutoresizingMaskIntoConstraints = false
stack.axis = .vertical
stack.spacing = 8
stack.alignment = .center
addSubview(stack)
NSLayoutConstraint.activate([
stack.centerXAnchor.constraint(equalTo: centerXAnchor),
stack.centerYAnchor.constraint(equalTo: centerYAnchor),
playImageView.widthAnchor.constraint(equalToConstant: 50),
playImageView.heightAnchor.constraint(equalToConstant: 50),
])
}
func loadGifv() {
let attachmentURL = self.attachment.url
let asset = AVURLAsset(url: attachmentURL)
AttachmentView.queue.async {
let generator = AVAssetImageGenerator(asset: asset)
generator.appliesPreferredTrackTransform = true
guard let image = try? generator.copyCGImage(at: .zero, actualTime: nil) else { return }
self.source = .cgImage(attachmentURL, image)
self.displayImage()
}
let gifvView = GifvAttachmentView(asset: asset, gravity: .resizeAspectFill)
self.gifvView = gifvView
gifvView.translatesAutoresizingMaskIntoConstraints = false
if autoplayGifs {
gifvView.player.play()
}
addSubview(gifvView)
NSLayoutConstraint.activate([
gifvView.leadingAnchor.constraint(equalTo: leadingAnchor),
gifvView.trailingAnchor.constraint(equalTo: trailingAnchor),
gifvView.topAnchor.constraint(equalTo: topAnchor),
gifvView.bottomAnchor.constraint(equalTo: bottomAnchor),
])
}
private func displayImage() {
isGrayscale = Preferences.shared.grayscaleImages
let image: UIImage?
switch source {
case nil:
image = nil
case let .imageData(url, data), let .gifData(url, data):
if isGrayscale {
image = ImageGrayscalifier.convert(url: url, data: data)
} else {
image = UIImage(data: data)
}
case let .cgImage(url, cgImage):
if isGrayscale {
image = ImageGrayscalifier.convert(url: url, cgImage: cgImage)
} else {
image = UIImage(cgImage: cgImage)
}
}
DispatchQueue.main.async {
self.image = image
}
}
func showGallery() {
if let delegate = delegate,
let gallery = delegate.attachmentViewGallery(startingAt: index) {
delegate.attachmentViewPresent(gallery, animated: true)
}
}
@objc func imagePressed() {
showGallery()
}
// MARK: - Accessibility
override func accessibilityActivate() -> Bool {
showGallery()
return true
}
}
fileprivate extension AttachmentView {
enum Source {
case imageData(URL, Data)
case gifData(URL, Data)
case cgImage(URL, CGImage)
}
}
extension AttachmentView: UIContextMenuInteractionDelegate {
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {
return UIContextMenuConfiguration(identifier: nil, previewProvider: { () -> UIViewController? in
if self.attachment.kind == .image {
return AttachmentPreviewViewController(sourceView: self)
} else if self.attachment.kind == .gifv {
let vc = GifvAttachmentViewController(attachment: self.attachment)
vc.preferredContentSize = self.image?.size ?? .zero
return vc
} else {
return self.delegate?.attachmentViewGallery(startingAt: self.index)
}
}, actionProvider: nil)
}
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) {
animator.addCompletion {
animator.preferredCommitStyle = .pop
if let gallery = animator.previewViewController as? GalleryViewController {
self.delegate?.attachmentViewPresent(gallery, animated: true)
} else {
self.showGallery()
}
}
}
}