From 1c9b1b9ac3a6b52fcce052de71a8a2ad93d202ff Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Tue, 12 May 2020 21:46:08 -0400 Subject: [PATCH] Add support (sort of) for gifv attachments See #98 --- Tusker.xcodeproj/project.pbxproj | 8 +++ .../GalleryViewController.swift | 2 + .../GifvAttachmentViewController.swift | 33 +++++++++++++ Tusker/Views/Attachments/AttachmentView.swift | 32 ++++++++++-- .../AttachmentsContainerView.swift | 2 +- .../Attachments/GifvAttachmentView.swift | 49 +++++++++++++++++++ 6 files changed, 122 insertions(+), 4 deletions(-) create mode 100644 Tusker/Screens/Attachment Gallery/GifvAttachmentViewController.swift create mode 100644 Tusker/Views/Attachments/GifvAttachmentView.swift diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index 5809c0e5..ec63d6ce 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -134,6 +134,8 @@ D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */; }; D64D8CA92463B494006B0BAA /* CachedDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D8CA82463B494006B0BAA /* CachedDictionary.swift */; }; D64F80E2215875CC00BEF393 /* XCBActionType.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64F80E1215875CC00BEF393 /* XCBActionType.swift */; }; + D6531DEE246B81C9000F9538 /* GifvAttachmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6531DED246B81C9000F9538 /* GifvAttachmentView.swift */; }; + D6531DF0246B867E000F9538 /* GifvAttachmentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6531DEF246B867E000F9538 /* GifvAttachmentViewController.swift */; }; D6538945214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6538944214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift */; }; D65A37F321472F300087646E /* SwiftSoup.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D6BED16E212663DA00F02DA0 /* SwiftSoup.framework */; }; D65F613423AEAB6600F3CFD3 /* OnboardingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65F613323AEAB6600F3CFD3 /* OnboardingTests.swift */; }; @@ -428,6 +430,8 @@ D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewController.swift; sourceTree = ""; }; D64D8CA82463B494006B0BAA /* CachedDictionary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CachedDictionary.swift; sourceTree = ""; }; D64F80E1215875CC00BEF393 /* XCBActionType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCBActionType.swift; sourceTree = ""; }; + D6531DED246B81C9000F9538 /* GifvAttachmentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GifvAttachmentView.swift; sourceTree = ""; }; + D6531DEF246B867E000F9538 /* GifvAttachmentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GifvAttachmentViewController.swift; sourceTree = ""; }; D6538944214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewSwipeActionProvider.swift; sourceTree = ""; }; D65F612D23AE990C00F3CFD3 /* Embassy.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Embassy.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D65F613023AE99E000F3CFD3 /* Ambassador.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Ambassador.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -597,6 +601,7 @@ children = ( 04D14BAE22B34A2800642648 /* GalleryViewController.swift */, D647D92724257BEB0005044F /* AttachmentPreviewViewController.swift */, + D6531DEF246B867E000F9538 /* GifvAttachmentViewController.swift */, ); path = "Attachment Gallery"; sourceTree = ""; @@ -1216,6 +1221,7 @@ isa = PBXGroup; children = ( D6C94D882139E6EC00CB5196 /* AttachmentView.swift */, + D6531DED246B81C9000F9538 /* GifvAttachmentView.swift */, D6C7D27C22B6EBF800071952 /* AttachmentsContainerView.swift */, ); path = Attachments; @@ -1650,6 +1656,7 @@ buildActionMask = 2147483647; files = ( D626493523BD94CE00612E6E /* CompositionAttachmentData.swift in Sources */, + D6531DF0246B867E000F9538 /* GifvAttachmentViewController.swift in Sources */, D6285B5321EA708700FE4B39 /* StatusFormat.swift in Sources */, D6DD353D22F28CD000A9563A /* ContentWarningCopyMode.swift in Sources */, 0427033A22B31269000D31B6 /* AdvancedPrefsView.swift in Sources */, @@ -1702,6 +1709,7 @@ D6B053A623BD2D0C00A066FA /* AssetCollectionViewController.swift in Sources */, 04DACE8E212CC7CC009840C4 /* ImageCache.swift in Sources */, D627FF7B217E951500CC0648 /* DraftsTableViewController.swift in Sources */, + D6531DEE246B81C9000F9538 /* GifvAttachmentView.swift in Sources */, D6370B9C24421FF30092A7FF /* Tusker.xcdatamodeld in Sources */, D6A3BC8F2321FFB900FD64D5 /* StatusActionAccountListTableViewController.swift in Sources */, D6AEBB4823216B1D00E5038B /* AccountActivity.swift in Sources */, diff --git a/Tusker/Screens/Attachment Gallery/GalleryViewController.swift b/Tusker/Screens/Attachment Gallery/GalleryViewController.swift index befaec37..e678b538 100644 --- a/Tusker/Screens/Attachment Gallery/GalleryViewController.swift +++ b/Tusker/Screens/Attachment Gallery/GalleryViewController.swift @@ -75,6 +75,8 @@ class GalleryViewController: UIPageViewController, UIPageViewControllerDataSourc let vc = AVPlayerViewController() vc.player = AVPlayer(url: $0.url) return vc + case .gifv: + return GifvAttachmentViewController(attachment: $0) default: fatalError() } diff --git a/Tusker/Screens/Attachment Gallery/GifvAttachmentViewController.swift b/Tusker/Screens/Attachment Gallery/GifvAttachmentViewController.swift new file mode 100644 index 00000000..11196108 --- /dev/null +++ b/Tusker/Screens/Attachment Gallery/GifvAttachmentViewController.swift @@ -0,0 +1,33 @@ +// +// GifvAttachmentViewController.swift +// Tusker +// +// Created by Shadowfacts on 5/12/20. +// Copyright © 2020 Shadowfacts. All rights reserved. +// + +import UIKit +import Pachyderm +import AVFoundation + +class GifvAttachmentViewController: UIViewController { + + private let attachment: Attachment + + init(attachment: Attachment) { + precondition(attachment.kind == .gifv) + self.attachment = attachment + + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func loadView() { + let asset = AVURLAsset(url: attachment.url) + self.view = GifvAttachmentView(asset: asset, gravity: .resizeAspect) + } + +} diff --git a/Tusker/Views/Attachments/AttachmentView.swift b/Tusker/Views/Attachments/AttachmentView.swift index 84386000..98d314d8 100644 --- a/Tusker/Views/Attachments/AttachmentView.swift +++ b/Tusker/Views/Attachments/AttachmentView.swift @@ -81,6 +81,8 @@ class AttachmentView: UIImageView, GIFAnimatable { loadVideo() case .audio: loadAudio() + case .gifv: + loadGifv() default: preconditionFailure("invalid attachment type") } @@ -111,9 +113,9 @@ class AttachmentView: UIImageView, GIFAnimatable { let asset = AVURLAsset(url: attachmentURL) 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 { - guard self.attachment.url == attachmentURL else { return } + guard let image = try? generator.copyCGImage(at: .zero, actualTime: nil) else { return } + DispatchQueue.main.async { [weak self] in + guard let self = self, self.attachment.url == attachmentURL else { return } self.image = UIImage(cgImage: image) } } @@ -150,6 +152,30 @@ class AttachmentView: UIImageView, GIFAnimatable { ]) } + func loadGifv() { + let attachmentURL = self.attachment.url + let asset = AVURLAsset(url: attachmentURL) + DispatchQueue.global(qos: .userInitiated).async { + let generator = AVAssetImageGenerator(asset: asset) + generator.appliesPreferredTrackTransform = true + guard let image = try? generator.copyCGImage(at: .zero, actualTime: nil) else { return } + DispatchQueue.main.async { [weak self] in + guard let self = self, self.attachment.url == attachmentURL else { return } + self.image = UIImage(cgImage: image) + } + } + + let gifvView = GifvAttachmentView(asset: asset, gravity: .resizeAspectFill) + gifvView.translatesAutoresizingMaskIntoConstraints = false + addSubview(gifvView) + NSLayoutConstraint.activate([ + gifvView.leadingAnchor.constraint(equalTo: leadingAnchor), + gifvView.trailingAnchor.constraint(equalTo: trailingAnchor), + gifvView.topAnchor.constraint(equalTo: topAnchor), + gifvView.bottomAnchor.constraint(equalTo: bottomAnchor), + ]) + } + override func display(_ layer: CALayer) { updateImageIfNeeded() } diff --git a/Tusker/Views/Attachments/AttachmentsContainerView.swift b/Tusker/Views/Attachments/AttachmentsContainerView.swift index 622f8ebc..08216ba8 100644 --- a/Tusker/Views/Attachments/AttachmentsContainerView.swift +++ b/Tusker/Views/Attachments/AttachmentsContainerView.swift @@ -11,7 +11,7 @@ import Pachyderm class AttachmentsContainerView: UIView { - static let supportedAttachmentTypes = [Attachment.Kind.image, .video, .audio] + static let supportedAttachmentTypes = [Attachment.Kind.image, .video, .audio, .gifv] weak var delegate: AttachmentViewDelegate? diff --git a/Tusker/Views/Attachments/GifvAttachmentView.swift b/Tusker/Views/Attachments/GifvAttachmentView.swift new file mode 100644 index 00000000..ab8ce20a --- /dev/null +++ b/Tusker/Views/Attachments/GifvAttachmentView.swift @@ -0,0 +1,49 @@ +// +// GifvAttachmentView.swift +// Tusker +// +// Created by Shadowfacts on 5/12/20. +// Copyright © 2020 Shadowfacts. All rights reserved. +// + +import UIKit +import AVFoundation + +class GifvAttachmentView: UIView { + + override class var layerClass: AnyClass { + return AVPlayerLayer.self + } + + private var playerLayer: AVPlayerLayer { + layer as! AVPlayerLayer + } + + private let item: AVPlayerItem + private let player: AVPlayer + + init(asset: AVAsset, gravity: AVLayerVideoGravity) { + item = AVPlayerItem(asset: asset) + player = AVPlayer(playerItem: item) + + super.init(frame: .zero) + + playerLayer.player = player + playerLayer.videoGravity = gravity + player.play() + + NotificationCenter.default.addObserver(self, selector: #selector(restartItem), name: .AVPlayerItemDidPlayToEndTime, object: item) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc func restartItem() { + item.seek(to: .zero) { (success) in + guard success else { return } + self.player.play() + } + } + +}