import UIKit final class RDEPUBNotePopupViewController: UIViewController { private let note: RDEPUBResolvedNote private let theme: RDEPUBReaderTheme private let onReturnToSource: (() -> Void)? private let onOpenNoteLocation: (() -> Void)? private let textView = UITextView() private let actionsStackView = UIStackView() init( note: RDEPUBResolvedNote, theme: RDEPUBReaderTheme, onReturnToSource: (() -> Void)? = nil, onOpenNoteLocation: (() -> Void)? = nil ) { self.note = note self.theme = theme self.onReturnToSource = onReturnToSource self.onOpenNoteLocation = onOpenNoteLocation super.init(nibName: nil, bundle: nil) title = note.title ?? "注释" } @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = theme.toolBackgroundColor navigationItem.rightBarButtonItem = UIBarButtonItem( barButtonSystemItem: .done, target: self, action: #selector(close) ) textView.translatesAutoresizingMaskIntoConstraints = false textView.isEditable = false textView.alwaysBounceVertical = true textView.backgroundColor = .clear textView.textContainerInset = UIEdgeInsets(top: 18, left: 18, bottom: 24, right: 18) textView.attributedText = attributedContent() textView.accessibilityIdentifier = "epub.reader.note.text" actionsStackView.translatesAutoresizingMaskIntoConstraints = false actionsStackView.axis = .horizontal actionsStackView.spacing = 12 actionsStackView.distribution = .fillEqually actionsStackView.accessibilityIdentifier = "epub.reader.note.actions" configureActionButtons() view.addSubview(actionsStackView) view.addSubview(textView) NSLayoutConstraint.activate([ actionsStackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 18), actionsStackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -18), actionsStackView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 12), actionsStackView.heightAnchor.constraint(equalToConstant: actionsStackView.arrangedSubviews.isEmpty ? 0 : 36), textView.leadingAnchor.constraint(equalTo: view.leadingAnchor), textView.trailingAnchor.constraint(equalTo: view.trailingAnchor), textView.topAnchor.constraint(equalTo: actionsStackView.bottomAnchor, constant: actionsStackView.arrangedSubviews.isEmpty ? 0 : 8), textView.bottomAnchor.constraint(equalTo: view.bottomAnchor) ]) } private func configureActionButtons() { if note.sourceLocation != nil, onReturnToSource != nil { actionsStackView.addArrangedSubview( makeActionButton(title: "返回原文", action: #selector(returnToSource)) ) } if onOpenNoteLocation != nil { actionsStackView.addArrangedSubview( makeActionButton(title: "打开注释原文", action: #selector(openNoteLocation)) ) } actionsStackView.isHidden = actionsStackView.arrangedSubviews.isEmpty } private func makeActionButton(title: String, action: Selector) -> UIButton { let button = UIButton(type: .system) var configuration = UIButton.Configuration.filled() configuration.cornerStyle = .medium configuration.title = title configuration.baseBackgroundColor = theme.toolControlTextColor.withAlphaComponent(0.10) configuration.baseForegroundColor = theme.toolControlTextColor button.configuration = configuration if title == "返回原文" { button.accessibilityIdentifier = "epub.reader.note.return" } else if title == "打开注释原文" { button.accessibilityIdentifier = "epub.reader.note.open" } button.addTarget(self, action: action, for: .touchUpInside) return button } private func attributedContent() -> NSAttributedString { let html = """ \(note.html) """ guard let data = html.data(using: .utf8), let attributed = try? NSMutableAttributedString( data: data, options: [ .documentType: NSAttributedString.DocumentType.html, .characterEncoding: String.Encoding.utf8.rawValue ], documentAttributes: nil ) else { return NSAttributedString(string: note.plainText) } return attributed } @objc private func close() { dismiss(animated: true) } @objc private func returnToSource() { dismiss(animated: true) { [onReturnToSource] in onReturnToSource?() } } @objc private func openNoteLocation() { dismiss(animated: true) { [onOpenNoteLocation] in onOpenNoteLocation?() } } }