// Copyright © 2020 Metabolist. All rights reserved. import AVKit import BlurHash import Combine import SDWebImage import UIKit import ViewModels final class AttachmentView: UIView { let playerView = PlayerView() let imageView = SDAnimatedImageView() let removeButton = UIButton(type: .close) let uncaptionedLabel = CapsuleLabel() let selectionButton = UIButton() var playing: Bool = false { didSet { if playing { play() imageView.tag = 0 playerView.tag = viewModel.tag } else { stop() imageView.tag = viewModel.tag playerView.tag = 0 } } } private let viewModel: AttachmentViewModel private let parentViewModel: AttachmentsRenderingViewModel private var playerCancellable: AnyCancellable? init(viewModel: AttachmentViewModel, parentViewModel: AttachmentsRenderingViewModel) { self.viewModel = viewModel self.parentViewModel = parentViewModel super.init(frame: .zero) initialSetup() } @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func layoutSubviews() { super.layoutSubviews() if let focus = viewModel.attachment.meta?.focus { let viewsAndMediaSizes: [(UIView, CGSize?)] = [ (imageView, imageView.image?.size), (playerView, playerView.player?.currentItem?.presentationSize)] for (view, mediaSize) in viewsAndMediaSizes { guard let size = mediaSize else { continue } view.setContentsRect(focus: focus, mediaSize: size) } } } } extension AttachmentView { func play() { let player = PlayerCache.shared.player(url: viewModel.attachment.url.url) playerCancellable = NotificationCenter.default.publisher( for: .AVPlayerItemDidPlayToEndTime, object: player.currentItem) .sink { _ in player.currentItem?.seek(to: .zero) { success in guard success else { return } player.play() } } player.isMuted = true player.play() playerView.player = player playerView.isHidden = false } func stop() { if let item = playerView.player?.currentItem { let imageGenerator = AVAssetImageGenerator(asset: item.asset) imageGenerator.requestedTimeToleranceAfter = .zero imageGenerator.requestedTimeToleranceBefore = .zero if let image = try? imageGenerator.copyCGImage(at: item.currentTime(), actualTime: nil) { imageView.image = .init(cgImage: image) } } playerView.player = nil playerView.isHidden = true } func selectAttachment() { parentViewModel.attachmentSelected(viewModel: viewModel) } } private extension AttachmentView { // swiftlint:disable:next function_body_length func initialSetup() { addSubview(imageView) imageView.translatesAutoresizingMaskIntoConstraints = false imageView.contentMode = .scaleAspectFill imageView.clipsToBounds = true imageView.autoPlayAnimatedImage = false imageView.tag = viewModel.tag let blurEffect = UIBlurEffect(style: .systemUltraThinMaterial) let playView = UIVisualEffectView(effect: blurEffect) let playVibrancyView = UIVisualEffectView(effect: UIVibrancyEffect(blurEffect: blurEffect)) let playImageView = UIImageView( image: UIImage(systemName: "play.circle", withConfiguration: UIImage.SymbolConfiguration(textStyle: .largeTitle))) playImageView.translatesAutoresizingMaskIntoConstraints = false playVibrancyView.translatesAutoresizingMaskIntoConstraints = false playVibrancyView.contentView.addSubview(playImageView) playView.contentView.addSubview(playVibrancyView) addSubview(playView) playView.translatesAutoresizingMaskIntoConstraints = false playView.clipsToBounds = true playView.layer.cornerRadius = .defaultCornerRadius playView.isHidden = viewModel.attachment.type == .image addSubview(playerView) playerView.translatesAutoresizingMaskIntoConstraints = false playerView.videoGravity = .resizeAspectFill playerView.isHidden = true addSubview(selectionButton) selectionButton.translatesAutoresizingMaskIntoConstraints = false selectionButton.setBackgroundImage(.highlightedButtonBackground, for: .highlighted) selectionButton.addAction( UIAction { [weak self] _ in self?.selectAttachment() }, for: .touchUpInside) selectionButton.accessibilityLabel = NSLocalizedString("compose.attachment.edit", comment: "") if let description = viewModel.attachment.description, !description.isEmpty { selectionButton.accessibilityLabel?.appendWithSeparator(description) } addSubview(removeButton) removeButton.translatesAutoresizingMaskIntoConstraints = false removeButton.showsMenuAsPrimaryAction = true removeButton.accessibilityLabel = NSLocalizedString("compose.attachment.remove", comment: "") removeButton.menu = UIMenu( children: [ UIAction( title: NSLocalizedString("remove", comment: ""), image: UIImage(systemName: "trash"), attributes: .destructive) { [weak self] _ in guard let self = self else { return } self.parentViewModel.removeAttachment(viewModel: self.viewModel) }]) addSubview(uncaptionedLabel) uncaptionedLabel.translatesAutoresizingMaskIntoConstraints = false uncaptionedLabel.text = NSLocalizedString("compose.attachment.uncaptioned", comment: "") uncaptionedLabel.isHidden = !(parentViewModel.canRemoveAttachments && (viewModel.attachment.description?.isEmpty ?? true)) switch viewModel.attachment.type { case .image, .video, .gifv: let placeholderImage: UIImage? if let blurHash = viewModel.attachment.blurhash { placeholderImage = UIImage(blurHash: blurHash, size: .blurHashSize) } else { placeholderImage = nil } imageView.sd_setImage( with: viewModel.attachment.previewUrl?.url, placeholderImage: placeholderImage) { [weak self] _, _, _, _ in self?.layoutSubviews() } case .audio: playImageView.image = UIImage(systemName: "waveform.circle", withConfiguration: UIImage.SymbolConfiguration(textStyle: .largeTitle)) backgroundColor = .secondarySystemBackground case .unknown: playImageView.image = UIImage(systemName: "link", withConfiguration: UIImage.SymbolConfiguration(textStyle: .largeTitle)) backgroundColor = .secondarySystemBackground } NSLayoutConstraint.activate([ imageView.leadingAnchor.constraint(equalTo: leadingAnchor), imageView.trailingAnchor.constraint(equalTo: trailingAnchor), imageView.topAnchor.constraint(equalTo: topAnchor), imageView.bottomAnchor.constraint(equalTo: bottomAnchor), playerView.leadingAnchor.constraint(equalTo: leadingAnchor), playerView.trailingAnchor.constraint(equalTo: trailingAnchor), playerView.topAnchor.constraint(equalTo: topAnchor), playerView.bottomAnchor.constraint(equalTo: bottomAnchor), playImageView.centerXAnchor.constraint(equalTo: playView.contentView.centerXAnchor), playImageView.centerYAnchor.constraint(equalTo: playView.contentView.centerYAnchor), playVibrancyView.leadingAnchor.constraint(equalTo: playView.leadingAnchor), playVibrancyView.topAnchor.constraint(equalTo: playView.topAnchor), playVibrancyView.trailingAnchor.constraint(equalTo: playView.trailingAnchor), playVibrancyView.bottomAnchor.constraint(equalTo: playView.bottomAnchor), playView.centerXAnchor.constraint(equalTo: centerXAnchor), playView.centerYAnchor.constraint(equalTo: centerYAnchor), playView.trailingAnchor.constraint( equalTo: playImageView.trailingAnchor, constant: .compactSpacing), playView.bottomAnchor.constraint( equalTo: playImageView.bottomAnchor, constant: .compactSpacing), playImageView.topAnchor.constraint( equalTo: playView.topAnchor, constant: .compactSpacing), playImageView.leadingAnchor.constraint( equalTo: playView.leadingAnchor, constant: .compactSpacing), selectionButton.leadingAnchor.constraint(equalTo: leadingAnchor), selectionButton.trailingAnchor.constraint(equalTo: trailingAnchor), selectionButton.topAnchor.constraint(equalTo: topAnchor), selectionButton.bottomAnchor.constraint(equalTo: bottomAnchor), removeButton.topAnchor.constraint(equalTo: layoutMarginsGuide.topAnchor), removeButton.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor), uncaptionedLabel.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor), uncaptionedLabel.bottomAnchor.constraint(equalTo: layoutMarginsGuide.bottomAnchor) ]) var accessibilityLabel = viewModel.attachment.type.accessibilityName if let description = viewModel.attachment.description { accessibilityLabel.appendWithSeparator(description) } self.accessibilityLabel = accessibilityLabel } }