chore: checkpoint current milestone work

This commit is contained in:
shen
2026-05-22 13:28:53 +08:00
parent 6d196d64e5
commit 5698aeaead
68 changed files with 5694 additions and 3291 deletions
@@ -25,6 +25,8 @@ public final class RDEPUBReaderController: UIViewController {
case pendingRepagination
}
private typealias NativeTextSnapshot = (pages: [EPUBPage], chapters: [EPUBChapterInfo])
public weak var delegate: RDEPUBReaderDelegate?
public var configuration: RDEPUBReaderConfiguration {
@@ -610,9 +612,10 @@ public final class RDEPUBReaderController: UIViewController {
private func applyTextBook(_ textBook: RDEPUBTextBook, restoreLocation: RDEPUBLocation?) {
self.textBook = textBook
self.activePages = []
self.activeChapters = []
readingSession?.setActiveSnapshot((pages: [], chapters: []))
let snapshot = nativeTextSnapshot(from: textBook)
self.activePages = snapshot.pages
self.activeChapters = snapshot.chapters
readingSession?.setActiveSnapshot(snapshot)
guard !textBook.pages.isEmpty else {
handle(error: RDEPUBParserError.emptySpine)
@@ -667,41 +670,28 @@ public final class RDEPUBReaderController: UIViewController {
@discardableResult
fileprivate func restoreReadingLocation(_ location: RDEPUBLocation, animated: Bool = false) -> Bool {
if let textBook {
guard let publication, let targetPageNumber = textBook.pageNumber(
for: location,
resolver: publication.resourceResolver,
bookIdentifier: currentBookIdentifier
) else {
readerView.transitionToPage(pageNum: 0)
readingSession?.transition(to: .idle)
return false
}
readerView.transitionToPage(pageNum: max(targetPageNumber - 1, 0), animated: animated)
guard let targetPageNumber = pageNumber(for: location) else {
readerView.transitionToPage(pageNum: 0)
readingSession?.transition(to: .idle)
return true
return false
}
guard let readingSession,
let targetPageNumber = readingSession.queueNavigation(
if textBook == nil {
_ = readingSession?.queueNavigation(
to: location,
relativeToSpineIndex: nil,
bookIdentifier: currentBookIdentifier
) else {
readerView.transitionToPage(pageNum: 0)
self.readingSession?.transition(to: .idle)
return false
)
} else {
readingSession?.transition(to: .jumping)
}
readerView.transitionToPage(pageNum: max(targetPageNumber - 1, 0), animated: animated)
return true
}
fileprivate func currentVisibleLocation() -> RDEPUBLocation? {
if let textBook, readerView.currentPage >= 0 {
return textBook.location(
forPageNumber: readerView.currentPage + 1,
bookIdentifier: currentBookIdentifier
)
if textBook != nil, readerView.currentPage >= 0 {
return resolvedTextLocation(forPageNumber: readerView.currentPage + 1)
}
return readingSession?.currentReadingLocation(bookIdentifier: currentBookIdentifier)
}
@@ -1494,6 +1484,17 @@ extension RDEPUBReaderController {
}
private func pageNumber(for searchMatch: RDEPUBSearchMatch) -> Int? {
if let textBook,
let publication,
let rangeLocation = searchMatch.rangeLocation,
let chapter = textBook.chapters.first(where: {
(publication.resourceResolver.normalizedHref($0.href) ?? $0.href) ==
(publication.resourceResolver.normalizedHref(searchMatch.href) ?? searchMatch.href)
}),
let page = chapter.pages.first(where: { rangeLocation >= $0.pageStartOffset && rangeLocation <= $0.pageEndOffset }) {
return page.absolutePageIndex + 1
}
let location = RDEPUBLocation(
bookIdentifier: currentBookIdentifier,
href: searchMatch.href,
@@ -1589,7 +1590,13 @@ extension RDEPUBReaderController: RDReaderDataSource, RDReaderDelegate {
}
private func textHighlights(for page: RDEPUBTextPage) -> [RDEPUBHighlight] {
activeHighlights.filter { $0.location.href == page.href }
guard let publication else {
return activeHighlights.filter { $0.location.href == page.href }
}
let pageHref = publication.resourceResolver.normalizedHref(page.href) ?? page.href
return activeHighlights.filter {
(publication.resourceResolver.normalizedHref($0.location.href) ?? $0.location.href) == pageHref
}
}
public func topToolView(readerView: RDReaderView) -> UIView? {
@@ -1608,10 +1615,10 @@ extension RDEPUBReaderController: RDReaderDataSource, RDReaderDelegate {
delegate?.epubReaderDidReachEnd(self)
}
if let textBook,
let location = textBook.location(forPageNumber: pageNum + 1, bookIdentifier: currentBookIdentifier) {
if textBook != nil,
let location = resolvedTextLocation(forPageNumber: pageNum + 1) {
persist(location: location)
readingSession?.transition(to: .idle)
synchronizeTextReadingState(pageNumber: pageNum + 1, location: location)
return
}
@@ -1707,16 +1714,18 @@ extension RDEPUBReaderController: RDEPUBTextContentViewDelegate {
return scopedSelection(selection, relativeToSpineIndex: nil)
}
let lastOffset = max(chapter.attributedContent.length - 1, 1)
let start = max(0, min(payload.start, lastOffset))
let end = max(start, min(payload.end, lastOffset))
let contentLength = max(chapter.attributedContent.length, 1)
let lastInclusiveOffset = max(contentLength - 1, 1)
let start = max(0, min(payload.start, lastInclusiveOffset))
let endExclusive = max(start + 1, min(payload.end, contentLength))
let lastSelectedOffset = max(start, min(endExclusive - 1, lastInclusiveOffset))
return RDEPUBSelection(
bookIdentifier: currentBookIdentifier,
location: RDEPUBLocation(
bookIdentifier: currentBookIdentifier,
href: selection.location.href,
progression: Double(start) / Double(lastOffset),
lastProgression: Double(end) / Double(lastOffset),
progression: Double(start) / Double(lastInclusiveOffset),
lastProgression: Double(lastSelectedOffset) / Double(lastInclusiveOffset),
fragment: nil
),
text: selection.text,
@@ -1724,6 +1733,81 @@ extension RDEPUBReaderController: RDEPUBTextContentViewDelegate {
createdAt: selection.createdAt
)
}
private func pageNumber(for location: RDEPUBLocation) -> Int? {
if let textBook, let publication {
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
)
}
private func resolvedTextLocation(forPageNumber pageNumber: Int) -> RDEPUBLocation? {
guard let textBook,
let publication,
let location = textBook.location(
forPageNumber: pageNumber,
bookIdentifier: currentBookIdentifier
) else {
return nil
}
return publication.resourceResolver.normalizedLocation(
location,
relativeToSpineIndex: nil,
bookIdentifier: currentBookIdentifier
) ?? location
}
private func synchronizeTextReadingState(pageNumber: Int, location: RDEPUBLocation) {
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
)
}
private func nativeTextSnapshot(from textBook: RDEPUBTextBook) -> NativeTextSnapshot {
let chapters = textBook.chapters.map {
EPUBChapterInfo(
spineIndex: $0.spineIndex,
title: $0.title,
pageCount: $0.pages.count
)
}
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)
}
}
extension RDEPUBReaderController: UIGestureRecognizerDelegate {
@@ -127,8 +127,9 @@ final class RDEPUBTextContentView: UIView {
}
private func applyHighlights(to content: NSMutableAttributedString, page: RDEPUBTextPage) {
let pageStart = Int(page.pageStartOffset)
let pageEndExclusive = Int(page.pageEndOffset) + 1
let pageRange = absoluteOffsetRange(for: page)
let pageStart = pageRange.lowerBound
let pageEndExclusive = pageRange.upperBound
for highlight in highlightedRanges where highlight.location.href == page.href {
guard let range = RDEPUBTextOffsetRangeInfo.decode(from: highlight.rangeInfo)?.nsRange else { continue }
@@ -165,8 +166,9 @@ final class RDEPUBTextContentView: UIView {
let normalColor = UIColor(red: 248 / 255, green: 225 / 255, blue: 108 / 255, alpha: 0.55)
let activeColor = UIColor(red: 255 / 255, green: 159 / 255, blue: 67 / 255, alpha: 0.75)
let pageStart = Int(page.pageStartOffset)
let pageEndExclusive = Int(page.pageEndOffset) + 1
let pageRange = absoluteOffsetRange(for: page)
let pageStart = pageRange.lowerBound
let pageEndExclusive = pageRange.upperBound
for match in searchState.matches where match.href == page.href {
guard let matchStart = match.rangeLocation else { continue }
@@ -181,6 +183,12 @@ final class RDEPUBTextContentView: UIView {
}
}
private func absoluteOffsetRange(for page: RDEPUBTextPage) -> Range<Int> {
let lowerBound = page.pageStartOffset
let upperBound = page.pageEndOffset + 1
return lowerBound..<max(upperBound, lowerBound)
}
}
extension RDEPUBTextContentView: UITextViewDelegate {