feat: EPUB阅读器搜索、注释、CFI模块及大书远距跳转优化

- 实现EPUB阅读器搜索功能及选中注释功能
- 优化CFI模块,修复代码审查发现的11个问题
- 实现大书远距目录跳转与后台补全优化方案
- 优化设置面板与章节运行时联动
- 重构及大量改进优化
This commit is contained in:
shenlei
2026-06-22 20:26:34 +08:00
parent f50495ad91
commit c65c190b71
178 changed files with 11380 additions and 6728 deletions
@@ -1,33 +1,31 @@
import Foundation
///
enum RDEPUBPageMapTakeoverDecision {
///
case keepCurrentWindow
///
case expandWindow(RDEPUBBackgroundCoverageSegment)
///
case segmentReplace(RDEPUBBackgroundCoverageSegment)
///
case fullReplace(RDEPUBBookPageMap)
}
///
final class RDEPUBPageMapReconciliationCoordinator {
private unowned let context: RDEPUBReaderContext
init(context: RDEPUBReaderContext) {
self.context = context
}
///
func evaluateTakeover(
candidatePageMap: RDEPUBBookPageMap?,
candidateSegment: RDEPUBBackgroundCoverageSegment?,
currentWindow: RDEPUBBookPageMap?,
jumpSession: RDEPUBJumpSession?
) -> RDEPUBPageMapTakeoverDecision {
//
guard let currentWindow else {
if let candidatePageMap {
return .fullReplace(candidatePageMap)
@@ -35,7 +33,6 @@ final class RDEPUBPageMapReconciliationCoordinator {
return .keepCurrentWindow
}
//
let currentSpineIndex = context.runtime?.locationCoordinator.currentVisibleLocation()
.flatMap { context.normalizedSpineIndex(for: $0) }
@@ -46,11 +43,10 @@ final class RDEPUBPageMapReconciliationCoordinator {
return item.linear && (item.mediaType.contains("html") || item.mediaType.contains("xhtml"))
}) ?? 0
// JumpSession
if let jumpSession {
let protectedIndices = jumpSession.protectedSpineIndices
if let currentSpineIndex, protectedIndices.contains(currentSpineIndex) {
//
if let candidateSegment {
let candidateIndices = candidateSegment.resolvedSpineIndices
let coverageRatio = Double(protectedIndices.intersection(candidateIndices).count) /
@@ -66,7 +62,6 @@ final class RDEPUBPageMapReconciliationCoordinator {
}
}
//
if let currentSpineIndex {
let requiresAdjacentCoverage = currentSpineIndex > 0 && currentSpineIndex < lastBuildableSpineIndex
@@ -85,7 +80,6 @@ final class RDEPUBPageMapReconciliationCoordinator {
}
}
//
if let candidateSegment {
let currentRenderSignature = context.currentRenderSignature()
if candidateSegment.renderSignature != currentRenderSignature {
@@ -97,7 +91,6 @@ final class RDEPUBPageMapReconciliationCoordinator {
}
}
//
if let candidateSegment {
return evaluateSegmentTakeover(
candidateSegment: candidateSegment,
@@ -119,7 +112,6 @@ final class RDEPUBPageMapReconciliationCoordinator {
return .keepCurrentWindow
}
///
private func evaluateSegmentTakeover(
candidateSegment: RDEPUBBackgroundCoverageSegment,
currentWindow: RDEPUBBookPageMap,
@@ -129,14 +121,12 @@ final class RDEPUBPageMapReconciliationCoordinator {
let currentIndices = Set(currentWindow.entries.map { $0.spineIndex })
let candidateIndices = candidateSegment.resolvedSpineIndices
//
if let currentSpineIndex {
if !candidateIndices.contains(currentSpineIndex) {
return .keepCurrentWindow
}
}
//
if let currentSpineIndex {
let hasPrev = candidateIndices.contains(currentSpineIndex - 1) || currentSpineIndex == 0
let hasNext = candidateIndices.contains(currentSpineIndex + 1) ||
@@ -146,21 +136,20 @@ final class RDEPUBPageMapReconciliationCoordinator {
}
}
//
let isContinuous = currentIndices.contains(candidateSegment.lowerSpineIndex - 1) ||
currentIndices.contains(candidateSegment.upperSpineIndex + 1) ||
candidateIndices.contains(currentWindow.entries.first?.spineIndex ?? Int.max) ||
candidateIndices.contains(currentWindow.entries.last?.spineIndex ?? Int.min)
if isContinuous {
//
return .expandWindow(candidateSegment)
} else {
//
let overlap = currentIndices.intersection(candidateIndices)
let overlapRatio = Double(overlap.count) / Double(currentIndices.count)
if overlapRatio > 0.5 {
//
return .segmentReplace(candidateSegment)
}
}
@@ -168,7 +157,6 @@ final class RDEPUBPageMapReconciliationCoordinator {
return .keepCurrentWindow
}
///
private func evaluateFullPageMapTakeover(
candidatePageMap: RDEPUBBookPageMap,
currentWindow: RDEPUBBookPageMap,
@@ -177,14 +165,12 @@ final class RDEPUBPageMapReconciliationCoordinator {
) -> RDEPUBPageMapTakeoverDecision {
let candidateIndices = Set(candidatePageMap.entries.map { $0.spineIndex })
//
if let currentSpineIndex {
if !candidateIndices.contains(currentSpineIndex) {
return .keepCurrentWindow
}
}
//
if let currentSpineIndex {
let hasPrev = candidateIndices.contains(currentSpineIndex - 1) || currentSpineIndex == 0
let hasNext = candidateIndices.contains(currentSpineIndex + 1) ||
@@ -194,7 +180,6 @@ final class RDEPUBPageMapReconciliationCoordinator {
}
}
//
let isComplete = candidateIndices.count >= currentWindow.entries.count
if isComplete {
return .fullReplace(candidatePageMap)
@@ -203,7 +188,6 @@ final class RDEPUBPageMapReconciliationCoordinator {
return .keepCurrentWindow
}
/// spineIndex
func protectedSpineIndices(
currentSpineIndex: Int?,
jumpSession: RDEPUBJumpSession?
@@ -212,7 +196,7 @@ final class RDEPUBPageMapReconciliationCoordinator {
if let currentSpineIndex {
indices.insert(currentSpineIndex)
//
if currentSpineIndex > 0 {
indices.insert(currentSpineIndex - 1)
}