Add support for video attachments

#7
This commit is contained in:
Shadowfacts 2019-09-10 12:25:50 -04:00
parent befcc18e4d
commit 6e4f89df4a
Signed by untrusted user: shadowfacts
GPG Key ID: 94A5AB95422746E5
9 changed files with 119 additions and 54 deletions

View File

@ -7,6 +7,8 @@
import UIKit import UIKit
import Pachyderm import Pachyderm
import AVFoundation
import AVKit
class GalleryViewController: UIPageViewController, UIPageViewControllerDataSource, UIPageViewControllerDelegate { class GalleryViewController: UIPageViewController, UIPageViewControllerDataSource, UIPageViewControllerDelegate {
@ -16,10 +18,10 @@ class GalleryViewController: UIPageViewController, UIPageViewControllerDataSourc
let sourcesInfo: [LargeImageViewController.SourceInfo?] let sourcesInfo: [LargeImageViewController.SourceInfo?]
let startIndex: Int let startIndex: Int
let pages: [AttachmentViewController] let pages: [UIViewController]
var currentIndex: Int { var currentIndex: Int {
guard let vc = viewControllers?.first as? AttachmentViewController, guard let vc = viewControllers?.first,
let index = pages.firstIndex(of: vc) else { let index = pages.firstIndex(of: vc) else {
fatalError() fatalError()
} }
@ -39,7 +41,18 @@ class GalleryViewController: UIPageViewController, UIPageViewControllerDataSourc
self.sourcesInfo = sourcesInfo self.sourcesInfo = sourcesInfo
self.startIndex = startIndex self.startIndex = startIndex
self.pages = attachments.map(AttachmentViewController.init) self.pages = attachments.map {
switch $0.kind {
case .image:
return AttachmentViewController(attachment: $0)
case .video:
let vc = AVPlayerViewController()
vc.player = AVPlayer(url: $0.url)
return vc
default:
fatalError()
}
}
super.init(transitionStyle: .scroll, navigationOrientation: .horizontal) super.init(transitionStyle: .scroll, navigationOrientation: .horizontal)
@ -61,12 +74,24 @@ class GalleryViewController: UIPageViewController, UIPageViewControllerDataSourc
dismissInteractionController = LargeImageInteractionController(viewController: self) dismissInteractionController = LargeImageInteractionController(viewController: self)
} }
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if let vc = pages[currentIndex] as? AVPlayerViewController {
// when the gallery is first shown, after the transition finishes, the controls for the player controller appear semi-transparent
// hiding the controls and then immediately reshowing them makes sure they're visible when the gallery is presented
vc.showsPlaybackControls = false
vc.showsPlaybackControls = true
// begin playing the video as soon as we appear
vc.player?.play()
}
}
// MARK: - Page View Controller Data Source // MARK: - Page View Controller Data Source
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? { func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
guard let attachment = viewController as? AttachmentViewController, guard let index = pages.firstIndex(of: viewController),
let index = pages.firstIndex(of: attachment),
index > 0 else { index > 0 else {
return nil return nil
} }
@ -74,8 +99,7 @@ class GalleryViewController: UIPageViewController, UIPageViewControllerDataSourc
} }
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? { func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
guard let attachment = viewController as? AttachmentViewController, guard let index = pages.firstIndex(of: viewController),
let index = pages.firstIndex(of: attachment),
index < pages.count - 1 else { index < pages.count - 1 else {
return nil return nil
} }
@ -84,9 +108,16 @@ class GalleryViewController: UIPageViewController, UIPageViewControllerDataSourc
// MARK: - Page View Controller Delegate // MARK: - Page View Controller Delegate
func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController]) { func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController]) {
let pending = pendingViewControllers.first as! AttachmentViewController if let pending = pendingViewControllers.first as? AttachmentViewController,
let current = viewControllers!.first as! AttachmentViewController let current = viewControllers!.first as? AttachmentViewController {
pending.controlsVisible = current.controlsVisible pending.controlsVisible = current.controlsVisible
}
if let pending = pendingViewControllers.first as? AVPlayerViewController {
// show controls and begin playing when the player page becomes visible
pending.showsPlaybackControls = true
pending.player?.play()
}
} }
} }

View File

@ -21,7 +21,7 @@ class GalleryExpandAnimationController: NSObject, UIViewControllerAnimatedTransi
} }
let finalVCFrame = transitionContext.finalFrame(for: toVC) let finalVCFrame = transitionContext.finalFrame(for: toVC)
guard let (sourceFrame, sourceCornerRadius) = toVC.sourcesInfo[toVC.startIndex] else { guard let (image, sourceFrame, sourceCornerRadius) = toVC.sourcesInfo[toVC.startIndex] else {
toVC.view.frame = finalVCFrame toVC.view.frame = finalVCFrame
transitionContext.completeTransition(!transitionContext.transitionWasCancelled) transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
return return
@ -29,11 +29,7 @@ class GalleryExpandAnimationController: NSObject, UIViewControllerAnimatedTransi
let attachment = toVC.attachments[toVC.startIndex] let attachment = toVC.attachments[toVC.startIndex]
guard let data = ImageCache.attachments.get(attachment.url), let image = UIImage(data: data) else { let containerView = transitionContext.containerView
toVC.view.frame = finalVCFrame
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
return
}
let ratio = image.size.width / image.size.height let ratio = image.size.width / image.size.height
var width = finalVCFrame.width var width = finalVCFrame.width
@ -46,11 +42,10 @@ class GalleryExpandAnimationController: NSObject, UIViewControllerAnimatedTransi
} }
let finalFrame = CGRect(x: finalVCFrame.midX - width / 2, y: finalVCFrame.midY - height / 2, width: width, height: height) let finalFrame = CGRect(x: finalVCFrame.midX - width / 2, y: finalVCFrame.midY - height / 2, width: width, height: height)
let containerView = transitionContext.containerView
let imageView = GIFImageView(frame: sourceFrame) let imageView = GIFImageView(frame: sourceFrame)
imageView.image = image imageView.image = image
if attachment.url.pathExtension == "gif" { if attachment.url.pathExtension == "gif",
let data = ImageCache.attachments.get(attachment.url) {
imageView.animate(withGIFData: data) imageView.animate(withGIFData: data)
} }
imageView.contentMode = .scaleAspectFill imageView.contentMode = .scaleAspectFill

View File

@ -26,7 +26,7 @@ class GalleryShrinkAnimationController: NSObject, UIViewControllerAnimatedTransi
return return
} }
guard let (sourceFrame, sourceCornerRadius) = fromVC.sourcesInfo[fromVC.currentIndex] else { guard let (image, sourceFrame, sourceCornerRadius) = fromVC.sourcesInfo[fromVC.currentIndex] else {
transitionContext.completeTransition(!transitionContext.transitionWasCancelled) transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
return return
} }
@ -34,12 +34,6 @@ class GalleryShrinkAnimationController: NSObject, UIViewControllerAnimatedTransi
let attachment = fromVC.attachments[fromVC.currentIndex] let attachment = fromVC.attachments[fromVC.currentIndex]
guard let data = ImageCache.attachments.get(attachment.url),
let image = UIImage(data: data) else {
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
return
}
let ratio = image.size.width / image.size.height let ratio = image.size.width / image.size.height
var width = originalVCFrame.width var width = originalVCFrame.width
var height = width / ratio var height = width / ratio
@ -53,7 +47,8 @@ class GalleryShrinkAnimationController: NSObject, UIViewControllerAnimatedTransi
let imageView = GIFImageView(frame: originalFrame) let imageView = GIFImageView(frame: originalFrame)
imageView.image = image imageView.image = image
if attachment.url.pathExtension == "gif" { if attachment.url.pathExtension == "gif",
let data = ImageCache.attachments.get(attachment.url) {
imageView.animate(withGIFData: data) imageView.animate(withGIFData: data)
} }
imageView.contentMode = .scaleAspectFill imageView.contentMode = .scaleAspectFill

View File

@ -13,7 +13,7 @@ import Gifu
class LargeImageViewController: UIViewController, UIScrollViewDelegate { class LargeImageViewController: UIViewController, UIScrollViewDelegate {
typealias SourceInfo = (frame: CGRect, cornerRadius: CGFloat) typealias SourceInfo = (image: UIImage, frame: CGRect, cornerRadius: CGFloat)
var sourceInfo: SourceInfo? var sourceInfo: SourceInfo?
var dismissInteractionController: LargeImageInteractionController? var dismissInteractionController: LargeImageInteractionController?

View File

@ -22,14 +22,13 @@ class LargeImageExpandAnimationController: NSObject, UIViewControllerAnimatedTra
} }
let finalVCFrame = transitionContext.finalFrame(for: toVC) let finalVCFrame = transitionContext.finalFrame(for: toVC)
guard let (originFrame, originCornerRadius) = toVC.sourceInfo else { guard let (image, originFrame, originCornerRadius) = toVC.sourceInfo else {
toVC.view.frame = finalVCFrame toVC.view.frame = finalVCFrame
transitionContext.completeTransition(!transitionContext.transitionWasCancelled) transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
return return
} }
let containerView = transitionContext.containerView let containerView = transitionContext.containerView
let image = toVC.imageView.image!
let ratio = image.size.width / image.size.height let ratio = image.size.width / image.size.height
let width = finalVCFrame.width let width = finalVCFrame.width
let height = width / ratio let height = width / ratio

View File

@ -27,7 +27,7 @@ class LargeImageShrinkAnimationController: NSObject, UIViewControllerAnimatedTra
return return
} }
guard let (finalFrame, finalCornerRadius) = fromVC.sourceInfo else { guard let (image, finalFrame, finalCornerRadius) = fromVC.sourceInfo else {
transitionContext.completeTransition(!transitionContext.transitionWasCancelled) transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
return return
} }
@ -35,14 +35,13 @@ class LargeImageShrinkAnimationController: NSObject, UIViewControllerAnimatedTra
let originalVCFrame = fromVC.view.frame let originalVCFrame = fromVC.view.frame
let containerView = transitionContext.containerView let containerView = transitionContext.containerView
let image = fromVC.image!
let ratio = image.size.width / image.size.height let ratio = image.size.width / image.size.height
let width = originalVCFrame.width let width = originalVCFrame.width
let height = width / ratio let height = width / ratio
let originalFrame = CGRect(x: originalVCFrame.midX - width / 2, y: originalVCFrame.midY - height / 2, width: width, height: height) let originalFrame = CGRect(x: originalVCFrame.midX - width / 2, y: originalVCFrame.midY - height / 2, width: width, height: height)
let imageView = GIFImageView(frame: originalFrame) let imageView = GIFImageView(frame: originalFrame)
imageView.image = fromVC.image! imageView.image = image
if let gifData = fromVC.gifData { if let gifData = fromVC.gifData {
imageView.animate(withGIFData: gifData) imageView.animate(withGIFData: gifData)
} }

View File

@ -28,17 +28,17 @@ protocol TuskerNavigationDelegate {
func reply(to statusID: String) func reply(to statusID: String)
func largeImage(_ image: UIImage, description: String?, sourceView: UIView) -> LargeImageViewController func largeImage(_ image: UIImage, description: String?, sourceView: UIImageView) -> LargeImageViewController
func largeImage(gifData: Data, description: String?, sourceView: UIView) -> LargeImageViewController func largeImage(gifData: Data, description: String?, sourceView: UIImageView) -> LargeImageViewController
func showLargeImage(_ image: UIImage, description: String?, animatingFrom sourceView: UIView) func showLargeImage(_ image: UIImage, description: String?, animatingFrom sourceView: UIImageView)
func showLargeImage(gifData: Data, description: String?, animatingFrom sourceView: UIView) func showLargeImage(gifData: Data, description: String?, animatingFrom sourceView: UIImageView)
func gallery(attachments: [Attachment], sourceViews: [UIView?], startIndex: Int) -> GalleryViewController func gallery(attachments: [Attachment], sourceViews: [UIImageView?], startIndex: Int) -> GalleryViewController
func showGallery(attachments: [Attachment], sourceViews: [UIView?], startIndex: Int) func showGallery(attachments: [Attachment], sourceViews: [UIImageView?], startIndex: Int)
func showMoreOptions(forStatus statusID: String) func showMoreOptions(forStatus statusID: String)
@ -109,7 +109,7 @@ extension TuskerNavigationDelegate where Self: UIViewController {
present(vc, animated: true) present(vc, animated: true)
} }
private func sourceViewInfo(_ sourceView: UIView?) -> LargeImageViewController.SourceInfo? { private func sourceViewInfo(_ sourceView: UIImageView?) -> LargeImageViewController.SourceInfo? {
guard let sourceView = sourceView else { return nil } guard let sourceView = sourceView else { return nil }
var sourceFrame = sourceView.convert(sourceView.bounds, to: view) var sourceFrame = sourceView.convert(sourceView.bounds, to: view)
@ -121,38 +121,38 @@ extension TuskerNavigationDelegate where Self: UIViewController {
let y = sourceFrame.minY * scale - scrollView.contentOffset.y + scrollView.frame.minY let y = sourceFrame.minY * scale - scrollView.contentOffset.y + scrollView.frame.minY
sourceFrame = CGRect(x: x, y: y, width: width, height: height) sourceFrame = CGRect(x: x, y: y, width: width, height: height)
} }
return (frame: sourceFrame, cornerRadius: sourceView.layer.cornerRadius) return (image: sourceView.image!, frame: sourceFrame, cornerRadius: sourceView.layer.cornerRadius)
} }
func largeImage(_ image: UIImage, description: String?, sourceView: UIView) -> LargeImageViewController { func largeImage(_ image: UIImage, description: String?, sourceView: UIImageView) -> LargeImageViewController {
let vc = LargeImageViewController(image: image, description: description, sourceInfo: sourceViewInfo(sourceView)) let vc = LargeImageViewController(image: image, description: description, sourceInfo: sourceViewInfo(sourceView))
vc.transitioningDelegate = self vc.transitioningDelegate = self
return vc return vc
} }
func largeImage(gifData: Data, description: String?, sourceView: UIView) -> LargeImageViewController { func largeImage(gifData: Data, description: String?, sourceView: UIImageView) -> LargeImageViewController {
let vc = LargeImageViewController(image: UIImage(data: gifData)!, description: description, sourceInfo: sourceViewInfo(sourceView)) let vc = LargeImageViewController(image: UIImage(data: gifData)!, description: description, sourceInfo: sourceViewInfo(sourceView))
vc.transitioningDelegate = self vc.transitioningDelegate = self
vc.gifData = gifData vc.gifData = gifData
return vc return vc
} }
func showLargeImage(_ image: UIImage, description: String?, animatingFrom sourceView: UIView) { func showLargeImage(_ image: UIImage, description: String?, animatingFrom sourceView: UIImageView) {
present(largeImage(image, description: description, sourceView: sourceView), animated: true) present(largeImage(image, description: description, sourceView: sourceView), animated: true)
} }
func showLargeImage(gifData: Data, description: String?, animatingFrom sourceView: UIView) { func showLargeImage(gifData: Data, description: String?, animatingFrom sourceView: UIImageView) {
present(largeImage(gifData: gifData, description: description, sourceView: sourceView), animated: true) present(largeImage(gifData: gifData, description: description, sourceView: sourceView), animated: true)
} }
func gallery(attachments: [Attachment], sourceViews: [UIView?], startIndex: Int) -> GalleryViewController { func gallery(attachments: [Attachment], sourceViews: [UIImageView?], startIndex: Int) -> GalleryViewController {
let sourcesInfo = sourceViews.map(sourceViewInfo) let sourcesInfo = sourceViews.map(sourceViewInfo)
let vc = GalleryViewController(attachments: attachments, sourcesInfo: sourcesInfo, startIndex: startIndex) let vc = GalleryViewController(attachments: attachments, sourcesInfo: sourcesInfo, startIndex: startIndex)
vc.transitioningDelegate = self vc.transitioningDelegate = self
return vc return vc
} }
func showGallery(attachments: [Attachment], sourceViews: [UIView?], startIndex: Int) { func showGallery(attachments: [Attachment], sourceViews: [UIImageView?], startIndex: Int) {
present(gallery(attachments: attachments, sourceViews: sourceViews, startIndex: startIndex), animated: true) present(gallery(attachments: attachments, sourceViews: sourceViews, startIndex: startIndex), animated: true)
} }

View File

@ -9,6 +9,7 @@
import UIKit import UIKit
import Pachyderm import Pachyderm
import Gifu import Gifu
import AVFoundation
protocol AttachmentViewDelegate { protocol AttachmentViewDelegate {
func showAttachmentsGallery(startingAt index: Int) func showAttachmentsGallery(startingAt index: Int)
@ -18,6 +19,8 @@ class AttachmentView: UIImageView, GIFAnimatable {
var delegate: AttachmentViewDelegate? var delegate: AttachmentViewDelegate?
var playImageView: UIImageView!
var attachment: Attachment! var attachment: Attachment!
var index: Int! var index: Int!
@ -31,7 +34,7 @@ class AttachmentView: UIImageView, GIFAnimatable {
self.attachment = attachment self.attachment = attachment
self.index = index self.index = index
loadImage() loadAttachment()
} }
required init?(coder aDecoder: NSCoder) { required init?(coder aDecoder: NSCoder) {
@ -44,6 +47,27 @@ class AttachmentView: UIImageView, GIFAnimatable {
layer.masksToBounds = true layer.masksToBounds = true
isUserInteractionEnabled = true isUserInteractionEnabled = true
addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(imagePressed))) addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(imagePressed)))
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 loadAttachment() {
switch attachment.kind {
case .image:
loadImage()
case .video:
loadVideo()
default:
fatalError()
}
} }
func loadImage() { func loadImage() {
@ -58,6 +82,22 @@ class AttachmentView: UIImageView, GIFAnimatable {
} }
} }
} }
playImageView.isHidden = true
}
func loadVideo() {
DispatchQueue.global(qos: .userInitiated).async {
let asset = AVURLAsset(url: self.attachment.url)
let generator = AVAssetImageGenerator(asset: asset)
generator.appliesPreferredTrackTransform = true
guard let image = try? generator.copyCGImage(at: CMTime(seconds: 0, preferredTimescale: 1), actualTime: nil) else { return }
DispatchQueue.main.async {
self.image = UIImage(cgImage: image)
}
}
playImageView.isHidden = false
} }
override func display(_ layer: CALayer) { override func display(_ layer: CALayer) {
@ -65,8 +105,14 @@ class AttachmentView: UIImageView, GIFAnimatable {
} }
@objc func imagePressed() { @objc func imagePressed() {
// delegate?.showLargeAttachment(for: self) // switch attachment.kind {
delegate?.showAttachmentsGallery(startingAt: index) // case .image:
delegate?.showAttachmentsGallery(startingAt: index)
// case .video:
// delegate?.showVideo(attachment: attachment)
// default:
// fatalError()
// }
} }
} }

View File

@ -48,7 +48,7 @@ class AttachmentsContainerView: UIView {
func updateUI(status: Status) { func updateUI(status: Status) {
self.statusID = status.id self.statusID = status.id
attachments = status.attachments.filter { $0.kind == .image } attachments = status.attachments.filter { $0.kind == .image || $0.kind == .video }
attachmentViews.removeAllObjects() attachmentViews.removeAllObjects()
subviews.forEach { $0.removeFromSuperview() } subviews.forEach { $0.removeFromSuperview() }