refactor: rename RDReaderView -> RDEpubReaderView, update pod config and docs
- Rename source module from RDReaderView to RDEpubReaderView - Move all source files from Sources/RDReaderView/ to Sources/RDEpubReaderView/ - Update podspec: RDReaderView.podspec -> RDEpubReaderView.podspec - Update Podfile, demo project, and CocoaPods config for new pod name - Delete old RDReaderView pod support files from ReadViewDemo/Pods - Add new RDEpubReaderView pod support files - Update documentation (API ref, architecture, UML, conventions, etc.) - Add FixedLayoutRotationTests - Update .gitignore: exclude .DS_Store, manual unpack backups, _ssoft-output
This commit is contained in:
+230
@@ -0,0 +1,230 @@
|
||||
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)
|
||||
}
|
||||
return .keepCurrentWindow
|
||||
}
|
||||
|
||||
let currentSpineIndex = context.runtime?.locationCoordinator.currentVisibleLocation()
|
||||
.flatMap { context.normalizedSpineIndex(for: $0) }
|
||||
|
||||
let lastBuildableSpineIndex = context.publication?.spine.indices
|
||||
.reversed()
|
||||
.first(where: { index in
|
||||
guard let item = context.publication?.spine[index] else { return false }
|
||||
return item.linear && (item.mediaType.contains("html") || item.mediaType.contains("xhtml"))
|
||||
}) ?? 0
|
||||
|
||||
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) /
|
||||
Double(protectedIndices.count)
|
||||
if coverageRatio < 0.8 {
|
||||
RDEPUBBackgroundTrace.log(
|
||||
"Reconciliation",
|
||||
"evaluateTakeover: keepCurrentWindow — jumpSession coverageRatio=\(String(format: "%.2f", coverageRatio)) protected=\(protectedIndices.count) candidateChapters=\(candidateIndices.count)"
|
||||
)
|
||||
return .keepCurrentWindow
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let currentSpineIndex {
|
||||
let requiresAdjacentCoverage = currentSpineIndex > 0 && currentSpineIndex < lastBuildableSpineIndex
|
||||
|
||||
if requiresAdjacentCoverage {
|
||||
if let candidateSegment {
|
||||
let hasPrev = candidateSegment.contains(spineIndex: currentSpineIndex - 1)
|
||||
let hasNext = candidateSegment.contains(spineIndex: currentSpineIndex + 1)
|
||||
if !hasPrev || !hasNext {
|
||||
RDEPUBBackgroundTrace.log(
|
||||
"Reconciliation",
|
||||
"evaluateTakeover: keepCurrentWindow — adjacentCoverage missing hasPrev=\(hasPrev) hasNext=\(hasNext) currentSpine=\(currentSpineIndex)"
|
||||
)
|
||||
return .keepCurrentWindow
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let candidateSegment {
|
||||
let currentRenderSignature = context.currentRenderSignature()
|
||||
if candidateSegment.renderSignature != currentRenderSignature {
|
||||
RDEPUBBackgroundTrace.log(
|
||||
"Reconciliation",
|
||||
"evaluateTakeover: keepCurrentWindow — renderSignature mismatch segment=\(candidateSegment.renderSignature) current=\(currentRenderSignature)"
|
||||
)
|
||||
return .keepCurrentWindow
|
||||
}
|
||||
}
|
||||
|
||||
if let candidateSegment {
|
||||
return evaluateSegmentTakeover(
|
||||
candidateSegment: candidateSegment,
|
||||
currentWindow: currentWindow,
|
||||
currentSpineIndex: currentSpineIndex,
|
||||
lastBuildableSpineIndex: lastBuildableSpineIndex
|
||||
)
|
||||
}
|
||||
|
||||
if let candidatePageMap {
|
||||
return evaluateFullPageMapTakeover(
|
||||
candidatePageMap: candidatePageMap,
|
||||
currentWindow: currentWindow,
|
||||
currentSpineIndex: currentSpineIndex,
|
||||
lastBuildableSpineIndex: lastBuildableSpineIndex
|
||||
)
|
||||
}
|
||||
|
||||
return .keepCurrentWindow
|
||||
}
|
||||
|
||||
private func evaluateSegmentTakeover(
|
||||
candidateSegment: RDEPUBBackgroundCoverageSegment,
|
||||
currentWindow: RDEPUBBookPageMap,
|
||||
currentSpineIndex: Int?,
|
||||
lastBuildableSpineIndex: Int
|
||||
) -> RDEPUBPageMapTakeoverDecision {
|
||||
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) ||
|
||||
currentSpineIndex == lastBuildableSpineIndex
|
||||
if !hasPrev || !hasNext {
|
||||
return .keepCurrentWindow
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
return .keepCurrentWindow
|
||||
}
|
||||
|
||||
private func evaluateFullPageMapTakeover(
|
||||
candidatePageMap: RDEPUBBookPageMap,
|
||||
currentWindow: RDEPUBBookPageMap,
|
||||
currentSpineIndex: Int?,
|
||||
lastBuildableSpineIndex: Int
|
||||
) -> RDEPUBPageMapTakeoverDecision {
|
||||
let candidateIndices = Set(candidatePageMap.entries.map { $0.spineIndex })
|
||||
let currentIndices = Set(currentWindow.entries.map { $0.spineIndex })
|
||||
let currentEntries = currentWindow.entries.count
|
||||
|
||||
if let currentSpineIndex {
|
||||
if !candidateIndices.contains(currentSpineIndex) {
|
||||
RDEPUBBackgroundTrace.log(
|
||||
"Reconciliation",
|
||||
"evaluateFullPageMapTakeover: keepCurrentWindow — currentSpine=\(currentSpineIndex) not in candidate chapters=\(candidateIndices.count)"
|
||||
)
|
||||
return .keepCurrentWindow
|
||||
}
|
||||
}
|
||||
|
||||
if let currentSpineIndex {
|
||||
let hasPrev = candidateIndices.contains(currentSpineIndex - 1) || currentSpineIndex == 0
|
||||
let hasNext = candidateIndices.contains(currentSpineIndex + 1) ||
|
||||
currentSpineIndex == lastBuildableSpineIndex
|
||||
if !hasPrev || !hasNext {
|
||||
RDEPUBBackgroundTrace.log(
|
||||
"Reconciliation",
|
||||
"evaluateFullPageMapTakeover: keepCurrentWindow — adjacentCoverage missing hasPrev=\(hasPrev) hasNext=\(hasNext) currentSpine=\(currentSpineIndex)"
|
||||
)
|
||||
return .keepCurrentWindow
|
||||
}
|
||||
}
|
||||
|
||||
let coversCurrentWindow = currentIndices.isSubset(of: candidateIndices)
|
||||
if coversCurrentWindow {
|
||||
RDEPUBBackgroundTrace.log(
|
||||
"Reconciliation",
|
||||
"evaluateFullPageMapTakeover: fullReplace — candidateChapters=\(candidateIndices.count) currentChapters=\(currentEntries) candidatePages=\(candidatePageMap.totalPages) currentPages=\(currentWindow.totalPages)"
|
||||
)
|
||||
return .fullReplace(candidatePageMap)
|
||||
}
|
||||
|
||||
RDEPUBBackgroundTrace.log(
|
||||
"Reconciliation",
|
||||
"evaluateFullPageMapTakeover: keepCurrentWindow — candidate does not cover currentWindow candidateChapters=\(candidateIndices.count) currentChapters=\(currentEntries)"
|
||||
)
|
||||
return .keepCurrentWindow
|
||||
}
|
||||
|
||||
func protectedSpineIndices(
|
||||
currentSpineIndex: Int?,
|
||||
jumpSession: RDEPUBJumpSession?
|
||||
) -> Set<Int> {
|
||||
var indices: Set<Int> = []
|
||||
|
||||
if let currentSpineIndex {
|
||||
indices.insert(currentSpineIndex)
|
||||
|
||||
if currentSpineIndex > 0 {
|
||||
indices.insert(currentSpineIndex - 1)
|
||||
}
|
||||
indices.insert(currentSpineIndex + 1)
|
||||
}
|
||||
|
||||
if let jumpSession {
|
||||
indices.formUnion(jumpSession.protectedSpineIndices)
|
||||
}
|
||||
|
||||
return indices
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user