- 拆分 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>
224 lines
8.6 KiB
Swift
224 lines
8.6 KiB
Swift
|
|
import UIKit
|
|
|
|
extension RDEPUBReaderController {
|
|
|
|
func pageNumber(for location: RDEPUBLocation) -> Int? {
|
|
if let publication,
|
|
let bookPageMap = readerContext.bookPageMap,
|
|
let spineIndex = readerContext.normalizedSpineIndex(for: location),
|
|
let entry = bookPageMap.entry(forSpineIndex: spineIndex) {
|
|
let normalizedLocation = publication.resourceResolver.normalizedLocation(
|
|
location,
|
|
relativeToSpineIndex: nil,
|
|
bookIdentifier: currentBookIdentifier
|
|
) ?? location
|
|
let localPageIndex = resolvedLocalPageIndex(
|
|
for: normalizedLocation,
|
|
spineIndex: spineIndex,
|
|
fallbackEntry: entry
|
|
) ?? fallbackLocalPageIndex(for: normalizedLocation, pageCount: entry.pageCount)
|
|
return bookPageMap.absolutePageIndex(
|
|
spineIndex: spineIndex,
|
|
localPageIndex: min(max(localPageIndex, 0), max(entry.pageCount - 1, 0))
|
|
).map { $0 + 1 }
|
|
}
|
|
|
|
if let textBook, let publication {
|
|
if let anchor = location.rangeAnchor?.start {
|
|
if let page = textBook.indexTable.pageNumber(for: anchor, in: textBook) {
|
|
return page + 1
|
|
}
|
|
}
|
|
|
|
let normalizedLocation = publication.resourceResolver.normalizedLocation(
|
|
location,
|
|
relativeToSpineIndex: nil,
|
|
bookIdentifier: currentBookIdentifier
|
|
) ?? location
|
|
return textBook.pageNumber(
|
|
for: normalizedLocation,
|
|
resolver: publication.resourceResolver,
|
|
bookIdentifier: currentBookIdentifier
|
|
)
|
|
}
|
|
|
|
return readingSession?.queueNavigation(
|
|
to: location,
|
|
relativeToSpineIndex: nil,
|
|
bookIdentifier: currentBookIdentifier
|
|
)
|
|
}
|
|
|
|
func resolvedTextLocation(forPageNumber pageNumber: Int) -> RDEPUBLocation? {
|
|
if let resolvedPage = resolvedRuntimePage(forPageNumber: pageNumber) {
|
|
let startOffset = resolvedPage.page.pageStartOffset
|
|
let endOffset = max(startOffset, resolvedPage.page.pageEndOffset)
|
|
let chapterData = makeRuntimeChapterData(from: resolvedPage)
|
|
let location = chapterData.location(
|
|
for: NSRange(location: startOffset, length: max(endOffset - startOffset + 1, 1)),
|
|
bookIdentifier: currentBookIdentifier
|
|
)
|
|
if let publication {
|
|
return publication.resourceResolver.normalizedLocation(
|
|
location,
|
|
relativeToSpineIndex: nil,
|
|
bookIdentifier: currentBookIdentifier
|
|
) ?? location
|
|
}
|
|
return location
|
|
}
|
|
|
|
guard let textBook,
|
|
let page = textBook.page(at: pageNumber) else {
|
|
return nil
|
|
}
|
|
|
|
let location = textBook.chapterData(forPageNumber: pageNumber)?.location(forPage: page, bookIdentifier: currentBookIdentifier)
|
|
?? textBook.location(forPageNumber: pageNumber, bookIdentifier: currentBookIdentifier)
|
|
guard let location else { return nil }
|
|
|
|
if let publication {
|
|
return publication.resourceResolver.normalizedLocation(
|
|
location,
|
|
relativeToSpineIndex: nil,
|
|
bookIdentifier: currentBookIdentifier
|
|
) ?? location
|
|
}
|
|
return location
|
|
}
|
|
|
|
func synchronizeTextReadingState(pageNumber: Int, location: RDEPUBLocation) {
|
|
if let resolvedPage = resolvedRuntimePage(forPageNumber: pageNumber) {
|
|
readingSession?.updateReadingContext(
|
|
pageNumber: pageNumber,
|
|
location: location,
|
|
spineIndex: resolvedPage.page.spineIndex,
|
|
chapterIndex: resolvedPage.chapterIndex,
|
|
bookIdentifier: currentBookIdentifier
|
|
)
|
|
return
|
|
}
|
|
|
|
guard let textBook,
|
|
let page = textBook.page(at: pageNumber) else {
|
|
readingSession?.transition(to: .idle)
|
|
return
|
|
}
|
|
|
|
readingSession?.updateReadingContext(
|
|
pageNumber: pageNumber,
|
|
location: location,
|
|
spineIndex: page.spineIndex,
|
|
chapterIndex: page.chapterIndex,
|
|
bookIdentifier: currentBookIdentifier
|
|
)
|
|
}
|
|
|
|
func nativeTextSnapshot(from textBook: RDEPUBTextBook) -> RDEPUBNativeTextSnapshot {
|
|
let chapters = textBook.chapterInfos
|
|
let pages = textBook.pages.map {
|
|
EPUBPage(
|
|
spineIndex: $0.spineIndex,
|
|
chapterIndex: $0.chapterIndex,
|
|
pageIndexInChapter: $0.pageIndexInChapter,
|
|
totalPagesInChapter: $0.totalPagesInChapter,
|
|
chapterTitle: $0.chapterTitle,
|
|
fixedSpread: nil
|
|
)
|
|
}
|
|
return (pages, chapters)
|
|
}
|
|
|
|
func chapterOffset(for location: RDEPUBLocation, fallbackEntry: RDEPUBBookPageMapEntry) -> Int {
|
|
if let spineIndex = readerContext.normalizedSpineIndex(for: location),
|
|
let runtimeChapter = runtime.chapterRuntimeStore.chapterData(for: spineIndex),
|
|
let cfi = primaryLocationCFI(for: location),
|
|
let offset = runtimeChapter.chapterOffsetMap.chapterOffset(forCFI: cfi) {
|
|
return offset
|
|
}
|
|
if let rawCFI = primaryLocationCFI(for: location),
|
|
let cfi = RDEPUBCFICompatibility.parseLossy(rawCFI),
|
|
let cfiOffset = RDEPUBCFIResolver.resolve(cfi).chapterOffset {
|
|
return cfiOffset
|
|
}
|
|
if let anchor = location.rangeAnchor?.start {
|
|
return anchor.chapterOffset
|
|
}
|
|
if let fragment = location.fragment,
|
|
let offset = fallbackEntry.fragmentOffsets[fragment] {
|
|
return offset
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func fallbackLocalPageIndex(for location: RDEPUBLocation, pageCount: Int) -> Int {
|
|
guard pageCount > 1 else { return 0 }
|
|
return min(
|
|
pageCount - 1,
|
|
max(0, Int(round(location.navigationProgression * Double(pageCount - 1))))
|
|
)
|
|
}
|
|
|
|
private func resolvedRuntimePage(forPageNumber pageNumber: Int) -> RDEPUBResolvedPage? {
|
|
runtime.pageResolver.resolvePage(absolutePageIndex: pageNumber - 1)
|
|
}
|
|
|
|
private func resolvedLocalPageIndex(
|
|
for location: RDEPUBLocation,
|
|
spineIndex: Int,
|
|
fallbackEntry: RDEPUBBookPageMapEntry
|
|
) -> Int? {
|
|
let offset = chapterOffset(for: location, fallbackEntry: fallbackEntry)
|
|
|
|
if let runtimeChapter = runtime.chapterRuntimeStore.chapterData(for: spineIndex),
|
|
let pageIndex = runtimeChapter.chapterOffsetMap.pageIndex(forChapterOffset: offset) {
|
|
return pageIndex
|
|
}
|
|
|
|
if let summary = readerContext.chapterSummary(forSpineIndex: spineIndex) {
|
|
return summary.pageRanges.firstIndex {
|
|
let range = $0.nsRange
|
|
return offset >= range.location && offset <= max(range.location + range.length - 1, range.location)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
private func primaryLocationCFI(for location: RDEPUBLocation) -> String? {
|
|
if let rangeCFI = RDEPUBCFICompatibility.parseRangeLossy(location.rangeCFI) {
|
|
return rangeCFI.start.rawValue
|
|
}
|
|
return location.cfi
|
|
}
|
|
|
|
private func nearestFragmentID(beforeOrAt offset: Int, fragmentOffsets: [String: Int]) -> String? {
|
|
var bestID: String?
|
|
var bestOffset = Int.min
|
|
for (fragmentID, fragmentOffset) in fragmentOffsets where fragmentOffset <= offset && fragmentOffset > bestOffset {
|
|
bestOffset = fragmentOffset
|
|
bestID = fragmentID
|
|
}
|
|
return bestID
|
|
}
|
|
|
|
private func makeRuntimeChapterData(from resolvedPage: RDEPUBResolvedPage) -> RDEPUBChapterData {
|
|
let textChapter = RDEPUBTextChapter(
|
|
chapterIndex: resolvedPage.chapterIndex,
|
|
spineIndex: resolvedPage.chapter.spineIndex,
|
|
href: resolvedPage.chapter.href,
|
|
title: resolvedPage.chapter.title,
|
|
attributedContent: resolvedPage.chapter.typesetAttributedString,
|
|
fragmentOffsets: resolvedPage.chapter.chapterOffsetMap.fragmentOffsets,
|
|
cfiMap: resolvedPage.chapter.chapterOffsetMap.cfiMap,
|
|
pageBreakReasons: resolvedPage.chapter.pages.map(\.metadata.breakReason),
|
|
pages: resolvedPage.chapter.pages
|
|
)
|
|
return RDEPUBChapterData(
|
|
chapter: textChapter,
|
|
indexTable: RDEPUBTextIndexTable(chapters: [textChapter])
|
|
)
|
|
}
|
|
}
|