Allow sharing gifv attachments, improve share sheet behavior for images
This commit is contained in:
parent
a5fc35d0b1
commit
cc401fce8c
|
@ -289,6 +289,7 @@
|
|||
D6CA6A92249FAD8900AD45C1 /* AudioSessionHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6CA6A91249FAD8900AD45C1 /* AudioSessionHelper.swift */; };
|
||||
D6CA6A94249FADE700AD45C1 /* GalleryPlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6CA6A93249FADE700AD45C1 /* GalleryPlayerViewController.swift */; };
|
||||
D6CA8CDA2962231F0050C433 /* ArrayUniqueTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6CA8CD92962231F0050C433 /* ArrayUniqueTests.swift */; };
|
||||
D6CA8CDE296387310050C433 /* SaveToPhotosActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6CA8CDD296387310050C433 /* SaveToPhotosActivity.swift */; };
|
||||
D6D12B2F2925D66500D528E1 /* TimelineGapCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D12B2E2925D66500D528E1 /* TimelineGapCollectionViewCell.swift */; };
|
||||
D6D12B56292D57E800D528E1 /* AccountCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D12B55292D57E800D528E1 /* AccountCollectionViewCell.swift */; };
|
||||
D6D12B58292D5B2C00D528E1 /* StatusActionAccountListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D12B57292D5B2C00D528E1 /* StatusActionAccountListViewController.swift */; };
|
||||
|
@ -674,6 +675,7 @@
|
|||
D6CA6A91249FAD8900AD45C1 /* AudioSessionHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioSessionHelper.swift; sourceTree = "<group>"; };
|
||||
D6CA6A93249FADE700AD45C1 /* GalleryPlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GalleryPlayerViewController.swift; sourceTree = "<group>"; };
|
||||
D6CA8CD92962231F0050C433 /* ArrayUniqueTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArrayUniqueTests.swift; sourceTree = "<group>"; };
|
||||
D6CA8CDD296387310050C433 /* SaveToPhotosActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SaveToPhotosActivity.swift; sourceTree = "<group>"; };
|
||||
D6D12B2E2925D66500D528E1 /* TimelineGapCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineGapCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||
D6D12B55292D57E800D528E1 /* AccountCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||
D6D12B57292D5B2C00D528E1 /* StatusActionAccountListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusActionAccountListViewController.swift; sourceTree = "<group>"; };
|
||||
|
@ -1299,6 +1301,7 @@
|
|||
D681E4D8246E346E0053414F /* AccountActivityItemSource.swift */,
|
||||
D6AEBB3D2321638100E5038B /* UIActivity+Types.swift */,
|
||||
D6AEBB422321685E00E5038B /* OpenInSafariActivity.swift */,
|
||||
D6CA8CDD296387310050C433 /* SaveToPhotosActivity.swift */,
|
||||
);
|
||||
path = Activities;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1974,6 +1977,7 @@
|
|||
D6E9CDA8281A427800BBC98E /* PostService.swift in Sources */,
|
||||
D6B053A223BD2C0600A066FA /* AssetPickerViewController.swift in Sources */,
|
||||
D6EE63FB2551F7F60065485C /* StatusCollapseButton.swift in Sources */,
|
||||
D6CA8CDE296387310050C433 /* SaveToPhotosActivity.swift in Sources */,
|
||||
D6B936712829F72900237D0E /* NSManagedObjectContext+Helpers.swift in Sources */,
|
||||
D68A76EE295369C7001DA1B3 /* AcknowledgementsView.swift in Sources */,
|
||||
D627944A23A6AD6100D38C68 /* BookmarksTableViewController.swift in Sources */,
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
//
|
||||
// SaveToPhotosActivity.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 1/2/23.
|
||||
// Copyright © 2023 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import UniformTypeIdentifiers
|
||||
import Photos
|
||||
|
||||
class SaveToPhotosActivity: UIActivity {
|
||||
|
||||
override class var activityCategory: UIActivity.Category {
|
||||
return .action
|
||||
}
|
||||
override var activityType: UIActivity.ActivityType? {
|
||||
return .saveToPhotos
|
||||
}
|
||||
|
||||
override var activityTitle: String? {
|
||||
return "Save to Photos"
|
||||
}
|
||||
override var activityImage: UIImage? {
|
||||
UIImage(systemName: "square.and.arrow.down")
|
||||
}
|
||||
|
||||
override func canPerform(withActivityItems activityItems: [Any]) -> Bool {
|
||||
return activityItems.contains(where: {
|
||||
if let url = $0 as? URL,
|
||||
let type = UTType(filenameExtension: url.pathExtension){
|
||||
return type.conforms(to: .movie) || type.conforms(to: .video) || type.conforms(to: .image)
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private var url: URL?
|
||||
private var type: UTType?
|
||||
|
||||
override func prepare(withActivityItems activityItems: [Any]) {
|
||||
for case let url as URL in activityItems {
|
||||
self.url = url
|
||||
type = UTType(filenameExtension: url.pathExtension)!
|
||||
}
|
||||
}
|
||||
|
||||
override func perform() {
|
||||
guard let url,
|
||||
let type else {
|
||||
return
|
||||
}
|
||||
PHPhotoLibrary.requestAuthorization(for: .addOnly) { status in
|
||||
guard case .authorized = status else {
|
||||
return
|
||||
}
|
||||
PHPhotoLibrary.shared().performChanges {
|
||||
if type.conforms(to: .movie) || type.conforms(to: .video) {
|
||||
_ = PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: url)
|
||||
} else {
|
||||
_ = PHAssetChangeRequest.creationRequestForAssetFromImage(atFileURL: url)
|
||||
}
|
||||
} completionHandler: { _, _ in
|
||||
self.activityDidFinish(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -11,5 +11,6 @@ import UIKit
|
|||
extension UIActivity.ActivityType {
|
||||
|
||||
static let openInSafari = UIActivity.ActivityType("\(Bundle.main.bundleIdentifier!).open_in_safari")
|
||||
static let saveToPhotos = UIActivity.ActivityType("\(Bundle.main.bundleIdentifier!).save_to_photos")
|
||||
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ class AccountListViewController: UIViewController, CollectionViewController {
|
|||
private let accountIDs: [String]
|
||||
|
||||
var collectionView: UICollectionView! {
|
||||
view as! UICollectionView
|
||||
view as? UICollectionView
|
||||
}
|
||||
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
||||
|
||||
|
|
|
@ -31,13 +31,20 @@ class LargeImageImageContentView: UIImageView, LargeImageContentView {
|
|||
|
||||
var animationImage: UIImage? { image! }
|
||||
var activityItemsForSharing: [Any] {
|
||||
[image!]
|
||||
guard let data else {
|
||||
return []
|
||||
}
|
||||
return [ImageActivityItemSource(data: data, url: url, image: image)]
|
||||
}
|
||||
weak var owner: LargeImageViewController?
|
||||
|
||||
private var sourceData: Data?
|
||||
private let url: URL
|
||||
private let data: Data?
|
||||
|
||||
init(url: URL, data: Data?, image: UIImage) {
|
||||
self.url = url
|
||||
self.data = data
|
||||
|
||||
init(image: UIImage) {
|
||||
super.init(image: image)
|
||||
|
||||
contentMode = .scaleAspectFit
|
||||
|
@ -79,7 +86,7 @@ class LargeImageImageContentView: UIImageView, LargeImageContentView {
|
|||
}
|
||||
|
||||
func grayscaleStateChanged() {
|
||||
guard let data = sourceData else {
|
||||
guard let data else {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -108,12 +115,15 @@ extension LargeImageImageContentView: ImageAnalysisInteractionDelegate {
|
|||
class LargeImageGifContentView: GIFImageView, LargeImageContentView {
|
||||
var animationImage: UIImage? { image }
|
||||
var activityItemsForSharing: [Any] {
|
||||
// todo: should gifs share the data?
|
||||
[image].compactMap { $0 }
|
||||
[ImageActivityItemSource(data: gifController!.gifData, url: url, image: image)]
|
||||
}
|
||||
weak var owner: LargeImageViewController?
|
||||
|
||||
init(gifController: GIFController) {
|
||||
private let url: URL
|
||||
|
||||
init(url: URL, gifController: GIFController) {
|
||||
self.url = url
|
||||
|
||||
super.init(image: gifController.lastFrame?.image)
|
||||
|
||||
contentMode = .scaleAspectFit
|
||||
|
@ -138,12 +148,11 @@ class LargeImageGifContentView: GIFImageView, LargeImageContentView {
|
|||
class LargeImageGifvContentView: GifvAttachmentView, LargeImageContentView {
|
||||
private(set) var animationImage: UIImage?
|
||||
var activityItemsForSharing: [Any] {
|
||||
// todo: what should we share for gifvs?
|
||||
// some SO posts indicate that just sharing a URL to the video should work, but that may need to be a local URL?
|
||||
[]
|
||||
[GifvActivityItemSource(asset: asset, attachment: attachment)]
|
||||
}
|
||||
weak var owner: LargeImageViewController?
|
||||
|
||||
private let attachment: Attachment
|
||||
private let asset: AVURLAsset
|
||||
|
||||
private var videoSize: CGSize?
|
||||
|
@ -154,6 +163,7 @@ class LargeImageGifvContentView: GifvAttachmentView, LargeImageContentView {
|
|||
init(attachment: Attachment, source: UIImageView) {
|
||||
precondition(attachment.kind == .gifv)
|
||||
|
||||
self.attachment = attachment
|
||||
self.asset = AVURLAsset(url: attachment.url)
|
||||
|
||||
super.init(asset: asset, gravity: .resizeAspect)
|
||||
|
@ -184,4 +194,78 @@ class LargeImageGifvContentView: GifvAttachmentView, LargeImageContentView {
|
|||
func grayscaleStateChanged() {
|
||||
// no-op, GifvAttachmentView observes the grayscale state itself
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fileprivate class ImageActivityItemSource: NSObject, UIActivityItemSource {
|
||||
let data: Data
|
||||
let url: URL
|
||||
let image: UIImage?
|
||||
|
||||
init(data: Data, url: URL, image: UIImage?) {
|
||||
self.data = data
|
||||
self.url = url
|
||||
self.image = image
|
||||
}
|
||||
|
||||
func activityViewControllerPlaceholderItem(_ activityViewController: UIActivityViewController) -> Any {
|
||||
return url
|
||||
}
|
||||
|
||||
func activityViewController(_ activityViewController: UIActivityViewController, thumbnailImageForActivityType activityType: UIActivity.ActivityType?, suggestedSize size: CGSize) -> UIImage? {
|
||||
return image
|
||||
}
|
||||
|
||||
func activityViewController(_ activityViewController: UIActivityViewController, itemForActivityType activityType: UIActivity.ActivityType?) -> Any? {
|
||||
do {
|
||||
let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent(url.lastPathComponent)
|
||||
try data.write(to: tempURL)
|
||||
return tempURL
|
||||
} catch {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func activityViewController(_ activityViewController: UIActivityViewController, dataTypeIdentifierForActivityType activityType: UIActivity.ActivityType?) -> String {
|
||||
return (UTType(filenameExtension: url.pathExtension) ?? .image).identifier
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate class GifvActivityItemSource: NSObject, UIActivityItemSource {
|
||||
let asset: AVAsset
|
||||
let attachment: Attachment
|
||||
|
||||
init(asset: AVAsset, attachment: Attachment) {
|
||||
self.asset = asset
|
||||
self.attachment = attachment
|
||||
}
|
||||
|
||||
func activityViewControllerPlaceholderItem(_ activityViewController: UIActivityViewController) -> Any {
|
||||
return attachment.url
|
||||
}
|
||||
|
||||
func activityViewController(_ activityViewController: UIActivityViewController, thumbnailImageForActivityType activityType: UIActivity.ActivityType?, suggestedSize size: CGSize) -> UIImage? {
|
||||
let generator = AVAssetImageGenerator(asset: self.asset)
|
||||
generator.appliesPreferredTrackTransform = true
|
||||
if let image = try? generator.copyCGImage(at: .zero, actualTime: nil) {
|
||||
return UIImage(cgImage: image)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func activityViewController(_ activityViewController: UIActivityViewController, itemForActivityType activityType: UIActivity.ActivityType?) -> Any? {
|
||||
do {
|
||||
let data = try Data(contentsOf: attachment.url)
|
||||
let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent(attachment.url.lastPathComponent)
|
||||
try data.write(to: tempURL)
|
||||
return tempURL
|
||||
} catch {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func activityViewController(_ activityViewController: UIActivityViewController, dataTypeIdentifierForActivityType activityType: UIActivity.ActivityType?) -> String {
|
||||
return (UTType(filenameExtension: attachment.url.pathExtension) ?? .video).identifier
|
||||
}
|
||||
}
|
||||
|
|
|
@ -381,7 +381,7 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma
|
|||
}
|
||||
|
||||
@IBAction func sharePressed(_ sender: Any) {
|
||||
let activityVC = UIActivityViewController(activityItems: contentView.activityItemsForSharing, applicationActivities: nil)
|
||||
let activityVC = UIActivityViewController(activityItems: contentView.activityItemsForSharing, applicationActivities: [SaveToPhotosActivity()])
|
||||
activityVC.popoverPresentationController?.sourceView = shareImage
|
||||
present(activityVC, animated: true)
|
||||
}
|
||||
|
|
|
@ -135,12 +135,12 @@ class LoadingLargeImageViewController: UIViewController, LargeImageAnimatableVie
|
|||
// is it possible for the source view's gif controller to have different data than we just got?
|
||||
// should this be a property set by the animation controller instead?
|
||||
let gifController = (animationSourceView as? GIFImageView)?.gifController ?? GIFController(gifData: data)
|
||||
content = LargeImageGifContentView(gifController: gifController)
|
||||
content = LargeImageGifContentView(url: url, gifController: gifController)
|
||||
} else {
|
||||
if let transformedImage = ImageGrayscalifier.convertIfNecessary(url: url, image: image) {
|
||||
content = LargeImageImageContentView(image: transformedImage)
|
||||
content = LargeImageImageContentView(url: url, data: data, image: transformedImage)
|
||||
} else {
|
||||
content = LargeImageImageContentView(image: image)
|
||||
content = LargeImageImageContentView(url: url, data: data, image: image)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -167,7 +167,7 @@ class LoadingLargeImageViewController: UIViewController, LargeImageAnimatableVie
|
|||
let grayscale = ImageGrayscalifier.convert(url: nil, cgImage: source) {
|
||||
image = grayscale
|
||||
}
|
||||
setContent(LargeImageImageContentView(image: image))
|
||||
setContent(LargeImageImageContentView(url: url, data: nil, image: image))
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue