右对齐文本显示修复(如"巫鸿"只显示"鸿"的问题): - RDEPUBChapterTailNormalizer: 短尾页合并时检查 avoidPageBreakInside 语义, 避免将带有此标记的短文本(如 right-info 署名)错误合并回上一页 - RDEPUBTextContentView: 续段归一化时跳过右对齐/居中对齐的段落, 防止错误修改 firstLineHeadIndent 导致首字不可见 - RDEPUBCSSCompatibilityLayer: 为含 text-align:right/center 的 CSS 块 自动注入 text-indent:0 !important,确保右对齐/居中文本无首行缩进 - RDEPUBTextRendererSupport: 排版属性归一化时将右对齐/居中段落的 firstLineHeadIndent 重置为 0 图片查看器及脚注点击功能: - 新增 RDEPUBImageViewController 和 RDEPUBImageViewerCoordinator, 支持从 WebView 和 TextPage 两种模式查看图片 - epub-bridge.js: 检测图片和脚注图片的点击事件,脚注图片显示弹窗 - RDEPUBJavaScriptBridge: 新增 imageDidTap/footnoteDidTap 桥接消息 - RDEPUBWebView: 新增图片和脚注点击的 delegate 方法 - RDEPUBAttachmentNormalizer: 改进脚注检测,优先使用 alt 文本判断 - RDEPUBPaginationModels: 新增 .footnote 附件类型 - RDEPUBPageLayoutSnapshot: 运行时动态解析附件类型,脚注优先级高于图片 位置解析改进: - RDEPUBReaderController+LocationResolution: 利用 rangeInfo 提升页码定位精度 其他: - RDEPUBTextBookCache: schema 版本升级至 13 - RDEPUBReaderTheme: 主题更新 - Pod 项目文件更新 Co-Authored-By: Claude <noreply@anthropic.com>
241 lines
8.5 KiB
Swift
241 lines
8.5 KiB
Swift
import UIKit
|
|
|
|
final class RDEPUBImageViewController: UIViewController {
|
|
|
|
struct Configuration {
|
|
let image: UIImage
|
|
let sourceRect: CGRect?
|
|
let altText: String?
|
|
let theme: RDEPUBReaderTheme
|
|
|
|
init(image: UIImage, sourceRect: CGRect? = nil, altText: String? = nil, theme: RDEPUBReaderTheme) {
|
|
self.image = image
|
|
self.sourceRect = sourceRect
|
|
self.altText = altText
|
|
self.theme = theme
|
|
}
|
|
}
|
|
|
|
// MARK: - Properties
|
|
|
|
private let configuration: Configuration
|
|
private let scrollView = UIScrollView()
|
|
private let imageView = UIImageView()
|
|
private let closeButton = UIButton(type: .system)
|
|
private let backgroundView = UIView()
|
|
|
|
private var isShowingChrome = true
|
|
|
|
// MARK: - Init
|
|
|
|
init(configuration: Configuration) {
|
|
self.configuration = configuration
|
|
super.init(nibName: nil, bundle: nil)
|
|
modalPresentationStyle = .fullScreen
|
|
modalTransitionStyle = .crossDissolve
|
|
}
|
|
|
|
@available(*, unavailable)
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
// MARK: - Lifecycle
|
|
|
|
override func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
setupBackground()
|
|
setupScrollView()
|
|
setupImageView()
|
|
setupCloseButton()
|
|
}
|
|
|
|
override func viewDidLayoutSubviews() {
|
|
super.viewDidLayoutSubviews()
|
|
centerImage()
|
|
}
|
|
|
|
override var prefersStatusBarHidden: Bool { true }
|
|
|
|
// MARK: - Setup
|
|
|
|
private func setupBackground() {
|
|
backgroundView.translatesAutoresizingMaskIntoConstraints = false
|
|
backgroundView.backgroundColor = configuration.theme.imageViewerBackgroundColor
|
|
view.addSubview(backgroundView)
|
|
NSLayoutConstraint.activate([
|
|
backgroundView.topAnchor.constraint(equalTo: view.topAnchor),
|
|
backgroundView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
|
backgroundView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
|
backgroundView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
|
|
])
|
|
|
|
let tap = UITapGestureRecognizer(target: self, action: #selector(dismissViewer))
|
|
backgroundView.addGestureRecognizer(tap)
|
|
}
|
|
|
|
private func setupScrollView() {
|
|
scrollView.translatesAutoresizingMaskIntoConstraints = false
|
|
scrollView.delegate = self
|
|
scrollView.minimumZoomScale = 1.0
|
|
scrollView.maximumZoomScale = 5.0
|
|
scrollView.showsVerticalScrollIndicator = false
|
|
scrollView.showsHorizontalScrollIndicator = false
|
|
scrollView.alwaysBounceVertical = false
|
|
scrollView.alwaysBounceHorizontal = false
|
|
scrollView.decelerationRate = .fast
|
|
view.addSubview(scrollView)
|
|
NSLayoutConstraint.activate([
|
|
scrollView.topAnchor.constraint(equalTo: view.topAnchor),
|
|
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
|
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
|
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
|
|
])
|
|
|
|
let swipeDown = UISwipeGestureRecognizer(target: self, action: #selector(dismissViewer))
|
|
swipeDown.direction = .down
|
|
scrollView.addGestureRecognizer(swipeDown)
|
|
}
|
|
|
|
private func setupImageView() {
|
|
imageView.image = configuration.image
|
|
imageView.contentMode = .scaleAspectFit
|
|
imageView.isUserInteractionEnabled = true
|
|
imageView.accessibilityLabel = configuration.altText
|
|
imageView.isAccessibilityElement = configuration.altText != nil
|
|
scrollView.addSubview(imageView)
|
|
|
|
let doubleTap = UITapGestureRecognizer(target: self, action: #selector(handleDoubleTap(_:)))
|
|
doubleTap.numberOfTapsRequired = 2
|
|
imageView.addGestureRecognizer(doubleTap)
|
|
|
|
let singleTap = UITapGestureRecognizer(target: self, action: #selector(toggleChrome))
|
|
singleTap.numberOfTapsRequired = 1
|
|
singleTap.require(toFail: doubleTap)
|
|
imageView.addGestureRecognizer(singleTap)
|
|
}
|
|
|
|
private func setupCloseButton() {
|
|
let config = UIImage.SymbolConfiguration(pointSize: 16, weight: .semibold)
|
|
closeButton.setImage(UIImage(systemName: "xmark", withConfiguration: config), for: .normal)
|
|
closeButton.tintColor = .white
|
|
closeButton.backgroundColor = UIColor(white: 0.3, alpha: 0.6)
|
|
closeButton.layer.cornerRadius = 16
|
|
closeButton.clipsToBounds = true
|
|
closeButton.addTarget(self, action: #selector(dismissViewer), for: .touchUpInside)
|
|
closeButton.accessibilityLabel = NSLocalizedString("Close image viewer", comment: "Accessibility label for close button in image viewer")
|
|
|
|
view.addSubview(closeButton)
|
|
closeButton.translatesAutoresizingMaskIntoConstraints = false
|
|
NSLayoutConstraint.activate([
|
|
closeButton.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 12),
|
|
closeButton.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -16),
|
|
closeButton.widthAnchor.constraint(equalToConstant: 32),
|
|
closeButton.heightAnchor.constraint(equalToConstant: 32)
|
|
])
|
|
}
|
|
|
|
// MARK: - Layout
|
|
|
|
private func centerImage() {
|
|
guard let image = imageView.image else { return }
|
|
let boundsSize = scrollView.bounds.size
|
|
guard boundsSize.width > 0, boundsSize.height > 0 else { return }
|
|
|
|
let imageSize = image.size
|
|
guard imageSize.width > 0, imageSize.height > 0 else { return }
|
|
|
|
let widthRatio = boundsSize.width / imageSize.width
|
|
let heightRatio = boundsSize.height / imageSize.height
|
|
let fitScale = min(widthRatio, heightRatio)
|
|
|
|
let fitWidth = imageSize.width * fitScale
|
|
let fitHeight = imageSize.height * fitScale
|
|
|
|
imageView.frame = CGRect(
|
|
x: 0,
|
|
y: 0,
|
|
width: fitWidth,
|
|
height: fitHeight
|
|
)
|
|
|
|
scrollView.contentSize = imageView.frame.size
|
|
|
|
let horizontalInset = max(0, (boundsSize.width - fitWidth) / 2)
|
|
let verticalInset = max(0, (boundsSize.height - fitHeight) / 2)
|
|
scrollView.contentInset = UIEdgeInsets(
|
|
top: verticalInset,
|
|
left: horizontalInset,
|
|
bottom: verticalInset,
|
|
right: horizontalInset
|
|
)
|
|
}
|
|
|
|
// MARK: - Actions
|
|
|
|
@objc private func handleDoubleTap(_ gesture: UITapGestureRecognizer) {
|
|
if scrollView.zoomScale > scrollView.minimumZoomScale {
|
|
scrollView.setZoomScale(scrollView.minimumZoomScale, animated: true)
|
|
} else {
|
|
let point = gesture.location(in: imageView)
|
|
let zoomSize = CGSize(width: 100, height: 100)
|
|
let zoomRect = CGRect(
|
|
x: point.x - zoomSize.width / 2,
|
|
y: point.y - zoomSize.height / 2,
|
|
width: zoomSize.width,
|
|
height: zoomSize.height
|
|
)
|
|
scrollView.zoom(to: zoomRect, animated: true)
|
|
}
|
|
}
|
|
|
|
@objc private func toggleChrome() {
|
|
isShowingChrome.toggle()
|
|
UIView.animate(withDuration: 0.25) {
|
|
self.closeButton.alpha = self.isShowingChrome ? 1.0 : 0.0
|
|
}
|
|
}
|
|
|
|
@objc private func dismissViewer() {
|
|
dismiss(animated: true)
|
|
}
|
|
}
|
|
|
|
// MARK: - UIScrollViewDelegate
|
|
|
|
extension RDEPUBImageViewController: UIScrollViewDelegate {
|
|
|
|
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
|
|
imageView
|
|
}
|
|
|
|
func scrollViewDidZoom(_ scrollView: UIScrollView) {
|
|
guard let image = imageView.image else { return }
|
|
let boundsSize = scrollView.bounds.size
|
|
let imageSize = image.size
|
|
guard imageSize.width > 0, imageSize.height > 0 else { return }
|
|
|
|
let widthRatio = boundsSize.width / imageSize.width
|
|
let heightRatio = boundsSize.height / imageSize.height
|
|
let fitScale = min(widthRatio, heightRatio)
|
|
|
|
let fitWidth = imageSize.width * fitScale * scrollView.zoomScale
|
|
let fitHeight = imageSize.height * fitScale * scrollView.zoomScale
|
|
|
|
imageView.frame = CGRect(
|
|
x: 0,
|
|
y: 0,
|
|
width: fitWidth,
|
|
height: fitHeight
|
|
)
|
|
|
|
let horizontalInset = max(0, (boundsSize.width - fitWidth) / 2)
|
|
let verticalInset = max(0, (boundsSize.height - fitHeight) / 2)
|
|
scrollView.contentInset = UIEdgeInsets(
|
|
top: verticalInset,
|
|
left: horizontalInset,
|
|
bottom: verticalInset,
|
|
right: horizontalInset
|
|
)
|
|
}
|
|
} |