Tusker/Tusker/Views/Status/StatusCardView.swift

260 lines
10 KiB
Swift

//
// 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<UITouch>, with event: UIEvent?) {
hStack.backgroundColor = activeBackgroundColor
setNeedsDisplay()
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
}
override func touchesEnded(_ touches: Set<UITouch>, 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<UITouch>, 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)
}
}
}
}
}