Tusker/Tusker/Screens/Large Image/LargeImageContentView.swift

273 lines
8.9 KiB
Swift

//
// LargeImageContentView.swift
// Tusker
//
// Created by Shadowfacts on 6/17/20.
// Copyright © 2020 Shadowfacts. All rights reserved.
//
import UIKit
import Pachyderm
@preconcurrency import AVFoundation
@preconcurrency import VisionKit
import TuskerComponents
protocol LargeImageContentView: UIView {
var animationImage: UIImage? { get }
var activityItemsForSharing: [Any] { get }
var owner: LargeImageViewController? { get set }
func setControlsVisible(_ controlsVisible: Bool)
func grayscaleStateChanged()
}
class LargeImageImageContentView: UIImageView, LargeImageContentView {
#if !targetEnvironment(macCatalyst)
@available(iOS 16.0, *)
private static let analyzer = ImageAnalyzer()
private var _analysisInteraction: AnyObject?
@available(iOS 16.0, *)
private var analysisInteraction: ImageAnalysisInteraction? { _analysisInteraction as? ImageAnalysisInteraction }
#endif
var animationImage: UIImage? { image! }
var activityItemsForSharing: [Any] {
guard let data else {
return []
}
return [ImageActivityItemSource(data: data, url: url, image: image)]
}
weak var owner: LargeImageViewController?
private let url: URL
private let data: Data?
init(url: URL, data: Data?, image: UIImage) {
self.url = url
self.data = data
super.init(image: image)
contentMode = .scaleAspectFit
isUserInteractionEnabled = true
#if !targetEnvironment(macCatalyst)
if #available(iOS 16.0, *),
ImageAnalyzer.isSupported {
let interaction = ImageAnalysisInteraction()
self._analysisInteraction = interaction
interaction.delegate = self
interaction.preferredInteractionTypes = .automatic
addInteraction(interaction)
Task {
do {
let result = try await LargeImageImageContentView.analyzer.analyze(image, configuration: ImageAnalyzer.Configuration([.text, .machineReadableCode]))
interaction.analysis = result
} catch {
// if analysis fails, we just don't show anything
}
}
}
#endif
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setControlsVisible(_ controlsVisible: Bool) {
#if !targetEnvironment(macCatalyst)
if #available(iOS 16.0, *),
let analysisInteraction {
// note: passing animated: true here doesn't seem to do anything by itself as of iOS 16.2 (20C5032e)
// so the LargeImageViewController handles animating, but we still need to pass true here otherwise it doesn't animate
analysisInteraction.setSupplementaryInterfaceHidden(!controlsVisible, animated: true)
}
#endif
}
func grayscaleStateChanged() {
guard let data else {
return
}
let image: UIImage?
if Preferences.shared.grayscaleImages {
image = ImageGrayscalifier.convert(url: nil, data: data)
} else {
image = UIImage(data: data)
}
if let image = image {
self.image = image
}
}
}
#if !targetEnvironment(macCatalyst)
@available(iOS 16.0, *)
extension LargeImageImageContentView: ImageAnalysisInteractionDelegate {
func presentingViewController(for interaction: ImageAnalysisInteraction) -> UIViewController? {
return owner
}
}
#endif
class LargeImageGifContentView: GIFImageView, LargeImageContentView {
var animationImage: UIImage? { image }
var activityItemsForSharing: [Any] {
[ImageActivityItemSource(data: gifController!.gifData, url: url, image: image)]
}
weak var owner: LargeImageViewController?
private let url: URL
init(url: URL, gifController: GIFController) {
self.url = url
super.init(image: gifController.lastFrame?.image)
contentMode = .scaleAspectFit
gifController.attach(to: self)
// todo: doing this in the init feels wrong
gifController.startAnimating()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setControlsVisible(_ controlsVisible: Bool) {
}
func grayscaleStateChanged() {
// todo
}
}
class LargeImageGifvContentView: GifvAttachmentView, LargeImageContentView {
private(set) var animationImage: UIImage?
var activityItemsForSharing: [Any] {
[GifvActivityItemSource(asset: asset, attachment: attachment)]
}
weak var owner: LargeImageViewController?
private let attachment: Attachment
private let asset: AVURLAsset
private var videoSize: CGSize?
override var intrinsicContentSize: CGSize {
videoSize ?? CGSize(width: UIView.noIntrinsicMetric, height: UIView.noIntrinsicMetric)
}
init(attachment: Attachment, source: UIImageView) {
precondition(attachment.kind == .gifv)
self.attachment = attachment
self.asset = AVURLAsset(url: attachment.url)
super.init(asset: asset, gravity: .resizeAspect)
self.animationImage = source.image
self.player.play()
Task {
do {
if let track = try await asset.loadTracks(withMediaType: .video).first {
let (size, transform) = try await track.load(.naturalSize, .preferredTransform)
self.videoSize = size.applying(transform)
self.invalidateIntrinsicContentSize()
}
} catch {
}
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setControlsVisible(_ controlsVisible: Bool) {
}
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
}
}