// // StatusCardView.swift // Tusker // // Created by Shadowfacts on 10/25/20. // Copyright © 2020 Shadowfacts. All rights reserved. // import UIKit import Pachyderm import SafariServices class StatusCardView: UIView { weak var navigationDelegate: TuskerNavigationDelegate? var card: Card? { didSet { if let card = card { self.updateUI(card: card) } } } private let activeBackgroundColor = UIColor.secondarySystemFill private let inactiveBackgroundColor = UIColor.secondarySystemBackground private var imageRequest: ImageCache.Request? private var isGrayscale = false private var titleLabel: UILabel! private var descriptionLabel: UILabel! private var imageView: UIImageView! private var placeholderImageView: UIImageView! override init(frame: CGRect) { super.init(frame: frame) commonInit() } required init?(coder: NSCoder) { super.init(coder: coder) commonInit() } private func commonInit() { self.clipsToBounds = true self.layer.cornerRadius = 6.5 self.layer.borderWidth = 1 self.layer.borderColor = UIColor.lightGray.cgColor self.backgroundColor = inactiveBackgroundColor self.addInteraction(UIContextMenuInteraction(delegate: self)) titleLabel = UILabel() titleLabel.font = UIFont(descriptor: UIFontDescriptor.preferredFontDescriptor(withTextStyle: .subheadline).withSymbolicTraits(.traitBold)!, size: 0) titleLabel.numberOfLines = 2 descriptionLabel = UILabel() descriptionLabel.font = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .caption1), size: 0) descriptionLabel.numberOfLines = 2 descriptionLabel.setContentCompressionResistancePriority(.defaultLow, for: .vertical) let vStack = UIStackView(arrangedSubviews: [ titleLabel, descriptionLabel ]) vStack.axis = .vertical vStack.alignment = .leading vStack.distribution = .fill vStack.spacing = 0 imageView = UIImageView() imageView.contentMode = .scaleAspectFill imageView.clipsToBounds = true let hStack = UIStackView(arrangedSubviews: [ imageView, vStack ]) hStack.translatesAutoresizingMaskIntoConstraints = false hStack.axis = .horizontal hStack.alignment = .center hStack.distribution = .fill hStack.spacing = 4 addSubview(hStack) placeholderImageView = UIImageView(image: UIImage(systemName: "doc.text")) placeholderImageView.translatesAutoresizingMaskIntoConstraints = false placeholderImageView.contentMode = .scaleAspectFit placeholderImageView.tintColor = .gray addSubview(placeholderImageView) NSLayoutConstraint.activate([ imageView.heightAnchor.constraint(equalTo: heightAnchor), imageView.widthAnchor.constraint(equalTo: imageView.heightAnchor), vStack.heightAnchor.constraint(equalTo: heightAnchor, constant: -8), hStack.leadingAnchor.constraint(equalTo: leadingAnchor), hStack.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -4), hStack.topAnchor.constraint(equalTo: topAnchor), hStack.bottomAnchor.constraint(equalTo: bottomAnchor), placeholderImageView.widthAnchor.constraint(equalToConstant: 30), placeholderImageView.heightAnchor.constraint(equalToConstant: 30), placeholderImageView.centerXAnchor.constraint(equalTo: imageView.centerXAnchor), placeholderImageView.centerYAnchor.constraint(equalTo: imageView.centerYAnchor), ]) NotificationCenter.default.addObserver(self, selector: #selector(updateUIForPreferences), name: .preferencesChanged, object: nil) } private func updateUI(card: Card) { self.imageView.image = nil updateGrayscaleableUI(card: card) updateUIForPreferences() let title = card.title.trimmingCharacters(in: .whitespacesAndNewlines) titleLabel.text = title titleLabel.isHidden = title.isEmpty let description = card.description.trimmingCharacters(in: .whitespacesAndNewlines) descriptionLabel.text = description descriptionLabel.isHidden = description.isEmpty } @objc private func updateUIForPreferences() { if isGrayscale != Preferences.shared.grayscaleImages, let card = card { updateGrayscaleableUI(card: card) } } private func updateGrayscaleableUI(card: Card) { isGrayscale = Preferences.shared.grayscaleImages if let imageURL = card.image { placeholderImageView.isHidden = true imageRequest = ImageCache.attachments.get(imageURL, completion: { (_, image) in guard let image = image, let transformedImage = ImageGrayscalifier.convertIfNecessary(url: imageURL, image: image) else { return } DispatchQueue.main.async { self.imageView.image = transformedImage } }) if imageRequest != nil { loadBlurHash() } } else { placeholderImageView.isHidden = false } } private func loadBlurHash() { guard let card = card, let hash = card.blurhash else { return } let imageViewSize = self.imageView.bounds.size // todo: merge this code with AttachmentView, use a single DispatchQueue DispatchQueue.global(qos: .default).async { [weak self] in guard let self = self else { return } let size: CGSize if let width = card.width, let height = card.height { size = CGSize(width: width, height: height) } else { size = imageViewSize } guard let preview = UIImage(blurHash: hash, size: size) else { return } DispatchQueue.main.async { [weak self] in guard let self = self, self.card?.url == card.url, self.imageView.image == nil else { return } self.imageView.image = preview } } } override func touchesBegan(_ touches: Set, with event: UIEvent?) { backgroundColor = activeBackgroundColor setNeedsDisplay() } override func touchesMoved(_ touches: Set, with event: UIEvent?) { } override func touchesEnded(_ touches: Set, with event: UIEvent?) { backgroundColor = inactiveBackgroundColor setNeedsDisplay() if let card = card, let delegate = navigationDelegate { delegate.selected(url: card.url) } } override func touchesCancelled(_ touches: Set, with event: UIEvent?) { backgroundColor = inactiveBackgroundColor setNeedsDisplay() } } extension StatusCardView: MenuPreviewProvider { } extension StatusCardView: UIContextMenuInteractionDelegate { func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? { guard let card = card else { return nil } return UIContextMenuConfiguration(identifier: nil) { return SFSafariViewController(url: card.url) } actionProvider: { (_) in let actions = self.actionsForURL(card.url, sourceView: self) return UIMenu(title: "", image: nil, identifier: nil, options: [], children: actions) } } func contextMenuInteraction(_ interaction: UIContextMenuInteraction, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) { if let viewController = animator.previewViewController, let delegate = navigationDelegate { animator.preferredCommitStyle = .pop animator.addCompletion { if let customPresenting = viewController as? CustomPreviewPresenting { customPresenting.presentFromPreview(presenter: delegate) } else { delegate.show(viewController) } } } } }