// // 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 @MainActor protocol LargeImageContentView: UIView { var animationImage: UIImage? { get } var activityItemsForSharing: [Any] { get } var owner: LargeImageViewController? { get set } func setControlsVisible(_ controlsVisible: Bool, animated: 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, animated: Bool) { #if !targetEnvironment(macCatalyst) if #available(iOS 16.0, *), let analysisInteraction { analysisInteraction.setSupplementaryInterfaceHidden(!controlsVisible, animated: animated) } #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, animated: 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, animated: 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? { #if os(visionOS) #warning("Use async AVAssetImageGenerator.image(at:)") return nil #else 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 } #endif } 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 } }