fix: 修复右对齐短文本显示不完整及多项功能改进
右对齐文本显示修复(如"巫鸿"只显示"鸿"的问题): - 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>
This commit is contained in:
@@ -200,10 +200,23 @@ struct RDEPUBPageLayoutSnapshot {
|
||||
let placement = page.metadata.attachmentPlacements.indices.contains(attachmentIndex)
|
||||
? page.metadata.attachmentPlacements[attachmentIndex]
|
||||
: nil
|
||||
let kind = page.metadata.attachmentKinds.indices.contains(attachmentIndex)
|
||||
let metadataKind = page.metadata.attachmentKinds.indices.contains(attachmentIndex)
|
||||
? page.metadata.attachmentKinds[attachmentIndex]
|
||||
: nil
|
||||
return (placement, kind)
|
||||
let resolvedKind = attachmentKind(at: range, on: page) ?? metadataKind
|
||||
return (placement, resolvedKind)
|
||||
}
|
||||
|
||||
private static func attachmentKind(
|
||||
at range: NSRange,
|
||||
on page: RDEPUBTextPage
|
||||
) -> RDEPUBTextAttachmentKind? {
|
||||
guard range.location >= 0,
|
||||
range.location < page.chapterContent.length else {
|
||||
return nil
|
||||
}
|
||||
let attributes = page.chapterContent.attributes(at: range.location, effectiveRange: nil)
|
||||
return RDEPUBAttachmentNormalizer.attachmentKind(for: attributes)
|
||||
}
|
||||
|
||||
private static func offset(_ range: NSRange, by offset: Int) -> NSRange {
|
||||
|
||||
@@ -19,6 +19,13 @@ protocol RDEPUBTextContentViewDelegate: AnyObject {
|
||||
sourceRect: CGRect,
|
||||
sourcePoint: CGPoint
|
||||
)
|
||||
|
||||
func textContentView(
|
||||
_ contentView: RDEPUBTextContentView,
|
||||
didActivateImage image: UIImage,
|
||||
sourceRect: CGRect,
|
||||
altText: String?
|
||||
)
|
||||
func textContentView(
|
||||
_ contentView: RDEPUBTextContentView,
|
||||
didRequestHighlightActions highlight: RDEPUBHighlight,
|
||||
@@ -26,6 +33,17 @@ protocol RDEPUBTextContentViewDelegate: AnyObject {
|
||||
)
|
||||
}
|
||||
|
||||
extension RDEPUBTextContentViewDelegate {
|
||||
func textContentView(
|
||||
_ contentView: RDEPUBTextContentView,
|
||||
didActivateImage image: UIImage,
|
||||
sourceRect: CGRect,
|
||||
altText: String?
|
||||
) {
|
||||
// Default: no-op. Implementors can present an image viewer.
|
||||
}
|
||||
}
|
||||
|
||||
final class RDEPUBTextContentView: UIView, UIGestureRecognizerDelegate, RDReaderCachePolicyProviding {
|
||||
|
||||
private var contentInsets: UIEdgeInsets = .zero
|
||||
@@ -508,6 +526,16 @@ final class RDEPUBTextContentView: UIView, UIGestureRecognizerDelegate, RDReader
|
||||
return nil
|
||||
}
|
||||
|
||||
private func imageFromPage(attachment: RDEPUBPageAttachment, page: RDEPUBTextPage) -> UIImage? {
|
||||
guard page.chapterContent.length > attachment.stringRange.location else { return nil }
|
||||
let attachmentValue = page.chapterContent.attribute(
|
||||
.attachment,
|
||||
at: attachment.stringRange.location,
|
||||
effectiveRange: nil
|
||||
)
|
||||
return image(from: attachmentValue)
|
||||
}
|
||||
|
||||
private func applyHighlightsToContent(
|
||||
_ content: NSMutableAttributedString,
|
||||
highlights: [RDEPUBHighlight],
|
||||
@@ -539,6 +567,9 @@ final class RDEPUBTextContentView: UIView, UIGestureRecognizerDelegate, RDReader
|
||||
let text = content.string as NSString
|
||||
let firstParagraphRange = text.paragraphRange(for: NSRange(location: 0, length: 0))
|
||||
guard firstParagraphRange.length > 0 else { return content }
|
||||
guard !firstParagraphUsesNonLeadingAlignment(in: content, range: firstParagraphRange) else {
|
||||
return content
|
||||
}
|
||||
|
||||
content.enumerateAttribute(.paragraphStyle, in: firstParagraphRange) { value, range, _ in
|
||||
guard let style = value as? NSParagraphStyle else { return }
|
||||
@@ -558,6 +589,21 @@ final class RDEPUBTextContentView: UIView, UIGestureRecognizerDelegate, RDReader
|
||||
return !CharacterSet.newlines.contains(previousScalar)
|
||||
}
|
||||
|
||||
private func firstParagraphUsesNonLeadingAlignment(
|
||||
in content: NSAttributedString,
|
||||
range: NSRange
|
||||
) -> Bool {
|
||||
var usesNonLeadingAlignment = false
|
||||
content.enumerateAttribute(.paragraphStyle, in: range) { value, _, stop in
|
||||
guard let style = value as? NSParagraphStyle else { return }
|
||||
if style.alignment == .right || style.alignment == .center {
|
||||
usesNonLeadingAlignment = true
|
||||
stop.pointee = true
|
||||
}
|
||||
}
|
||||
return usesNonLeadingAlignment
|
||||
}
|
||||
|
||||
#if canImport(DTCoreText)
|
||||
|
||||
private func updateCoreTextLayoutFrameIfNeeded() {
|
||||
@@ -650,6 +696,29 @@ final class RDEPUBTextContentView: UIView, UIGestureRecognizerDelegate, RDReader
|
||||
clearSelection()
|
||||
return
|
||||
}
|
||||
// PRIORITY: Check for attachment tap
|
||||
if let page = currentPage,
|
||||
let attachment = interactionController.snapshot?.attachment(at: point) {
|
||||
let kind = attachment.kind
|
||||
let sourceRect = attachmentSourceRect(at: point, fallbackPoint: point) ?? CGRect.zero
|
||||
// Footnote attachments carry their note text in alt/accessibility metadata.
|
||||
// Prefer that semantic text over image preview even if attachment kind metadata is incomplete.
|
||||
if let footnoteText = attachmentText(at: point), kind == .footnote || !footnoteText.isEmpty {
|
||||
delegate?.textContentView(
|
||||
self,
|
||||
didActivateAttachmentText: footnoteText,
|
||||
sourceRect: sourceRect,
|
||||
sourcePoint: convert(point, from: overlayView)
|
||||
)
|
||||
return
|
||||
}
|
||||
// Regular image attachments: present image viewer
|
||||
if let image = imageFromPage(attachment: attachment, page: page) {
|
||||
let altText = attachmentText(at: point)
|
||||
delegate?.textContentView(self, didActivateImage: image, sourceRect: sourceRect, altText: altText)
|
||||
return
|
||||
}
|
||||
}
|
||||
if let attachmentText = attachmentText(at: point),
|
||||
let sourceRect = attachmentSourceRect(at: point, fallbackPoint: point) {
|
||||
delegate?.textContentView(
|
||||
|
||||
Reference in New Issue
Block a user