feat: 交互协调器拆分、附件提示、暗色图片适配、选区放大镜及文档清理

- 拆分 ContentDelegates/TextContentView 为独立协调器(InteractionCoordinator、LocationResolution、ExternalLinks、AttachmentTooltip)
- 新增 RDEPUBAttachmentTooltipView/OverlayView 附件气泡提示
- 新增 RDEPUBDarkImageAdjuster 暗色模式图片亮度适配
- 新增 RDEPUBSelectionLoupeView 选区放大镜
- 新增 MetadataParseWorker/CancellationController 元数据解析取消机制
- 重构 PresentationRuntime/PaginationCoordinator 精简职责
- 优化 ChapterLoader/WarmupOrchestrator 异步章节加载
- CFI 模块微调与 NoteModels 更新
- 清理冗余文档,更新架构/UML/业务逻辑文档

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
shenlei
2026-06-24 17:47:24 +08:00
co-authored by Claude
parent 7de661eb54
commit d15f20b097
59 changed files with 4522 additions and 7220 deletions
@@ -75,7 +75,7 @@ public enum RDEPUBCFIDOMPathBuilder {
match.numberOfRanges > 2 else {
return nil
}
return nsAttributes.substring(with: match.range(at: 2)).rd_cfiNilIfEmpty
return nsAttributes.substring(with: match.range(at: 2)).rd_nilIfEmpty
}
static func isIgnorableTag(_ tagName: String) -> Bool {
@@ -138,8 +138,8 @@ enum RDEPUBCFITextNodeMapBuilder {
markers: markers,
pathRanges: pathRanges,
recoveryMetadata: RDEPUBCFIRecoveryMetadata(
domFingerprint: rawHTML.sha256Hex,
normalizedTextChecksum: normalizedChapter.normalizedText.sha256Hex,
domFingerprint: rawHTML.rd_sha256Hex,
normalizedTextChecksum: normalizedChapter.normalizedText.rd_sha256Hex,
tokenIndex: makeTokenAnchors(
normalizedChapter: normalizedChapter,
markers: markers,
@@ -516,14 +516,6 @@ private struct RDEPUBNormalizedTextIndex {
}
}
extension String {
var rd_cfiNilIfEmpty: String? {
let trimmed = trimmingCharacters(in: .whitespacesAndNewlines)
return trimmed.isEmpty ? nil : trimmed
}
}
private extension String {
var fnv1a64: UInt64 {
let offsetBasis: UInt64 = 14695981039346656037
@@ -114,7 +114,7 @@ public enum RDEPUBCFIParser {
if let open = rawStep.firstIndex(of: "["),
let close = rawStep.lastIndex(of: "]"),
open < close {
idAssertion = String(rawStep[rawStep.index(after: open)..<close]).rd_cfiNilIfEmpty
idAssertion = String(rawStep[rawStep.index(after: open)..<close]).rd_nilIfEmpty
} else {
idAssertion = nil
}
@@ -34,6 +34,6 @@ public struct RDEPUBCFIStep: Codable, Equatable, Hashable {
public init(index: Int, idAssertion: String? = nil) {
self.index = index
self.idAssertion = idAssertion?.rd_cfiNilIfEmpty
self.idAssertion = idAssertion?.rd_nilIfEmpty
}
}
@@ -28,7 +28,7 @@ public enum RDEPUBCFIResolver {
if cfi.packagePath.steps.count >= 3 {
manifestStep = cfi.packagePath.steps[2]
} else {
manifestStep = cfi.packagePath.steps.last(where: { $0.idAssertion?.rd_cfiNilIfEmpty != nil })
manifestStep = cfi.packagePath.steps.last(where: { $0.idAssertion?.rd_nilIfEmpty != nil })
}
let href = manifestStep?.idAssertion
@@ -40,7 +40,7 @@ public enum RDEPUBCFIResolver {
fileIndex = nil
}
let fragmentID = cfi.contentPath.steps.last(where: { $0.idAssertion?.rd_cfiNilIfEmpty != nil })?.idAssertion
let fragmentID = cfi.contentPath.steps.last(where: { $0.idAssertion?.rd_nilIfEmpty != nil })?.idAssertion
return RDEPUBCFIResolverResult(
href: href,
fileIndex: fileIndex,
@@ -41,7 +41,7 @@ public struct RDEPUBNoteReference: Codable, Equatable {
self.targetHref = targetHref
self.targetFragment = targetFragment
self.targetCFI = targetCFI
self.label = label?.rdNoteNilIfEmpty
self.label = label?.rd_nilIfEmpty
self.kind = kind
}
}
@@ -71,15 +71,8 @@ public struct RDEPUBResolvedNote: Equatable {
self.reference = reference
self.sourceLocation = sourceLocation
self.targetLocation = targetLocation
self.title = title?.rdNoteNilIfEmpty
self.title = title?.rd_nilIfEmpty
self.html = html
self.plainText = plainText
}
}
private extension String {
var rdNoteNilIfEmpty: String? {
let trimmed = trimmingCharacters(in: .whitespacesAndNewlines)
return trimmed.isEmpty ? nil : trimmed
}
}
@@ -261,7 +261,11 @@ public final class RDEPUBReadingSession {
href: location.href,
progression: location.progression,
lastProgression: location.lastProgression,
fragment: location.fragment
fragment: location.fragment,
rangeAnchor: location.rangeAnchor,
cfi: location.cfi,
lastCFI: location.lastCFI,
rangeCFI: location.rangeCFI
)
let resource = RDEPUBViewportResource(
@@ -96,15 +96,3 @@ extension RDEPUBWebView {
currentLoadSignature = nil
}
}
extension UIColor {
var ss_hexString: String {
var red: CGFloat = 0
var green: CGFloat = 0
var blue: CGFloat = 0
var alpha: CGFloat = 0
getRed(&red, green: &green, blue: &blue, alpha: &alpha)
return String(format: "#%02X%02X%02X", Int(red * 255), Int(green * 255), Int(blue * 255))
}
}
@@ -16,7 +16,7 @@ extension RDEPUBWebView {
spread: spread,
viewportSize: viewportSize,
contentInset: contentInset,
backgroundColorCSS: backgroundColor?.ss_hexString,
backgroundColorCSS: backgroundColor?.rd_hexString,
fit: .page,
searchPresentation: nil
)
@@ -50,7 +50,7 @@ extension RDEPUBWebView {
RDEPUBWebViewDebug.log(debugScope, message: "load fixed spread resources=\(request.spread.resources.map(\.href).joined(separator: ",")) fit=\(request.fit.rawValue)")
webView.loadHTMLString(
html,
baseURL: URL(string: "\(RDEPUBResourceURLSchemeHandler.scheme):///")
baseURL: URL(string: "\(RDEPUBResourceURLSchemeHandler.scheme)://\(RDEPUBResourceURLSchemeHandler.host)/")
)
}