// // StatusCardView.swift // Tusker // // Created by Shadowfacts on 10/25/20. // Copyright © 2020 Shadowfacts. All rights reserved. // import UIKit import Pachyderm import SafariServices import WebURLFoundationExtras import SwiftSoup class StatusCardView: UIView { weak var navigationDelegate: TuskerNavigationDelegate? weak var actionProvider: MenuActionProvider? private var statusID: String? private(set) var card: Card? private let activeBackgroundColor = UIColor.secondarySystemFill private let inactiveBackgroundColor = UIColor.secondarySystemBackground private var isGrayscale = false private var hStack: UIStackView! private var vStack: UIStackView! private var titleLabel: UILabel! private var descriptionLabel: UILabel! private var domainLabel: UILabel! private var imageView: CachedImageView! private var placeholderImageView: UIImageView! private var leadingSpacer: UIView! private var trailingSpacer: UIView! override init(frame: CGRect) { super.init(frame: frame) commonInit() } required init?(coder: NSCoder) { super.init(coder: coder) commonInit() } private func commonInit() { self.layer.shadowColor = UIColor.black.cgColor self.layer.shadowRadius = 5 self.layer.shadowOpacity = 0.2 self.layer.shadowOffset = .zero self.addInteraction(UIContextMenuInteraction(delegate: self)) titleLabel = UILabel() titleLabel.font = UIFont(descriptor: UIFontDescriptor.preferredFontDescriptor(withTextStyle: .subheadline).withSymbolicTraits(.traitBold)!, size: 0) titleLabel.adjustsFontForContentSizeCategory = true titleLabel.numberOfLines = 2 titleLabel.setContentHuggingPriority(.defaultHigh, for: .vertical) descriptionLabel = UILabel() descriptionLabel.font = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .caption1), size: 0) descriptionLabel.adjustsFontForContentSizeCategory = true descriptionLabel.numberOfLines = 3 descriptionLabel.setContentCompressionResistancePriority(.defaultLow, for: .vertical) domainLabel = UILabel() domainLabel.font = .preferredFont(forTextStyle: .caption2) domainLabel.adjustsFontForContentSizeCategory = true domainLabel.numberOfLines = 1 domainLabel.textColor = .tintColor vStack = UIStackView(arrangedSubviews: [ titleLabel, descriptionLabel, domainLabel, ]) vStack.axis = .vertical vStack.alignment = .leading vStack.spacing = 0 imageView = CachedImageView(cache: .attachments) imageView.contentMode = .scaleAspectFill imageView.clipsToBounds = true leadingSpacer = UIView() leadingSpacer.backgroundColor = .clear trailingSpacer = UIView() trailingSpacer.backgroundColor = .clear hStack = UIStackView(arrangedSubviews: [ leadingSpacer, imageView, vStack, trailingSpacer, ]) hStack.translatesAutoresizingMaskIntoConstraints = false hStack.axis = .horizontal hStack.alignment = .center hStack.distribution = .fill hStack.spacing = 4 hStack.clipsToBounds = true hStack.layer.borderWidth = 0.5 hStack.layer.cornerCurve = .continuous hStack.backgroundColor = inactiveBackgroundColor updateBorderColor() addSubview(hStack) placeholderImageView = UIImageView(image: UIImage(systemName: "doc.text")) placeholderImageView.translatesAutoresizingMaskIntoConstraints = false placeholderImageView.contentMode = .scaleAspectFit placeholderImageView.tintColor = .gray placeholderImageView.isHidden = true addSubview(placeholderImageView) NSLayoutConstraint.activate([ imageView.heightAnchor.constraint(equalTo: heightAnchor), imageView.widthAnchor.constraint(equalTo: imageView.heightAnchor), vStack.heightAnchor.constraint(lessThanOrEqualTo: heightAnchor, constant: -8), leadingSpacer.widthAnchor.constraint(equalToConstant: 4), trailingSpacer.widthAnchor.constraint(equalToConstant: 4), hStack.leadingAnchor.constraint(equalTo: leadingAnchor), hStack.trailingAnchor.constraint(equalTo: trailingAnchor), 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), ]) } override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { super.traitCollectionDidChange(previousTraitCollection) updateBorderColor() } override func layoutSubviews() { super.layoutSubviews() hStack.layer.cornerRadius = 0.1 * bounds.height } private func updateBorderColor() { if traitCollection.userInterfaceStyle == .dark { hStack.layer.borderColor = UIColor.darkGray.cgColor } else { hStack.layer.borderColor = UIColor.lightGray.cgColor } } func updateUI(status: StatusMO) { guard status.id != statusID else { return } self.card = status.card self.statusID = status.id guard let card = status.card else { return } if let image = card.image { imageView.update(for: URL(image), blurhash: card.blurhash) imageView.isHidden = false leadingSpacer.isHidden = true } else { imageView.update(for: nil) imageView.isHidden = true leadingSpacer.isHidden = false } let title = card.title.trimmingCharacters(in: .whitespacesAndNewlines) titleLabel.text = title titleLabel.isHidden = title.isEmpty let description = try! SwiftSoup.parseBodyFragment(card.description).text() descriptionLabel.text = description descriptionLabel.isHidden = description.isEmpty if let host = card.url.host { domainLabel.text = host.serialized domainLabel.isHidden = false } else { domainLabel.isHidden = true } let titleHeight = titleLabel.isHidden ? 0 : titleLabel.sizeThatFits(CGSize(width: titleLabel.bounds.width, height: UIView.layoutFittingCompressedSize.height)).height let descriptionHeight = descriptionLabel.isHidden ? 0 : descriptionLabel.sizeThatFits(CGSize(width: descriptionLabel.bounds.width, height: UIView.layoutFittingCompressedSize.height)).height let domainLabel = domainLabel.isHidden ? 0 : domainLabel.sizeThatFits(CGSize(width: domainLabel.bounds.width, height: UIView.layoutFittingCompressedSize.height)).height if titleHeight + descriptionHeight + domainLabel > vStack.bounds.height { descriptionLabel.isHidden = true } } override func touchesBegan(_ touches: Set, with event: UIEvent?) { hStack.backgroundColor = activeBackgroundColor setNeedsDisplay() } override func touchesMoved(_ touches: Set, with event: UIEvent?) { } override func touchesEnded(_ touches: Set, with event: UIEvent?) { hStack.backgroundColor = inactiveBackgroundColor setNeedsDisplay() if let card = card, let delegate = navigationDelegate { delegate.selected(url: URL(card.url)!) } } override func touchesCancelled(_ touches: Set, with event: UIEvent?) { hStack.backgroundColor = inactiveBackgroundColor setNeedsDisplay() } } extension StatusCardView: UIContextMenuInteractionDelegate { func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? { guard let card = card else { return nil } return UIContextMenuConfiguration(identifier: nil) { let vc = SFSafariViewController(url: URL(card.url)!) vc.preferredControlTintColor = Preferences.shared.accentColor.color return vc } actionProvider: { (_) in let actions = self.actionProvider?.actionsForURL(URL(card.url)!, source: .view(self)) ?? [] return UIMenu(title: "", image: nil, identifier: nil, options: [], children: actions) } } func contextMenuInteraction(_ interaction: UIContextMenuInteraction, previewForHighlightingMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? { let params = UIPreviewParameters() params.visiblePath = UIBezierPath(roundedRect: hStack.bounds, cornerRadius: hStack.layer.cornerRadius) return UITargetedPreview(view: hStack, parameters: params) } 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) } } } } }