feat(wxread): align pagination, rendering, and docs

This commit is contained in:
shen
2026-05-24 15:36:01 +08:00
parent a318c0e3d0
commit 2a94921e88
103 changed files with 6124 additions and 4623 deletions
@@ -58,6 +58,11 @@ public final class RDEPUBReaderController: UIViewController {
currentVisibleLocation()
}
public var currentPageNumber: Int? {
guard readerView.currentPage >= 0 else { return nil }
return readerView.currentPage + 1
}
public private(set) var currentSelection: RDEPUBSelection?
public var highlights: [RDEPUBHighlight] {
@@ -263,6 +268,27 @@ public final class RDEPUBReaderController: UIViewController {
restoreReadingLocation(location)
}
@discardableResult
public func go(toPageNumber pageNumber: Int, animated: Bool = false) -> Bool {
guard pageNumber > 0 else { return false }
if textBook != nil {
guard let location = resolvedTextLocation(forPageNumber: pageNumber) else {
return false
}
return restoreReadingLocation(location, animated: animated)
}
guard activePages.indices.contains(pageNumber - 1) else {
return false
}
readerView.transitionToPage(pageNum: pageNumber - 1, animated: animated)
if let location = currentVisibleLocation() {
persist(location: location)
}
return true
}
public func clearSelection() {
updateCurrentSelection(nil)
}
@@ -489,12 +515,17 @@ public final class RDEPUBReaderController: UIViewController {
}
private func applyReaderViewConfiguration() {
let displayTypeDidChange = readerView.currentDisplayType != configuration.displayType
let preservedLocation = displayTypeDidChange ? currentVisibleLocation() : nil
view.backgroundColor = configuration.theme.contentBackgroundColor
readerView.landscapeDualPageEnabled = configuration.landscapeDualPageEnabled
readerView.pageDirection = resolvedPageDirection()
updateReaderChrome()
if readerView.currentDisplayType != configuration.displayType {
if displayTypeDidChange {
readerView.switchReaderDisplayType(configuration.displayType)
if let preservedLocation {
_ = restoreReadingLocation(preservedLocation)
}
}
}
@@ -750,7 +781,8 @@ public final class RDEPUBReaderController: UIViewController {
href: selection.location.href,
progression: selection.location.progression,
lastProgression: selection.location.lastProgression,
fragment: selection.location.fragment
fragment: selection.location.fragment,
rangeAnchor: selection.location.rangeAnchor
)
return RDEPUBSelection(
bookIdentifier: currentBookIdentifier,
@@ -772,7 +804,8 @@ public final class RDEPUBReaderController: UIViewController {
href: highlight.location.href,
progression: highlight.location.progression,
lastProgression: highlight.location.lastProgression,
fragment: highlight.location.fragment
fragment: highlight.location.fragment,
rangeAnchor: highlight.location.rangeAnchor
)
return RDEPUBHighlight(
id: highlight.id,
@@ -1367,6 +1400,11 @@ extension RDEPUBReaderController {
return false
}
if let bookmarkAnchor = bookmark.location.rangeAnchor,
let locationAnchor = location.rangeAnchor {
return bookmarkAnchor == locationAnchor
}
if let bookmarkFragment = bookmark.location.fragment,
let locationFragment = location.fragment {
return bookmarkFragment == locationFragment
@@ -1394,7 +1432,8 @@ extension RDEPUBReaderController {
href: location.href,
progression: location.progression,
lastProgression: location.lastProgression,
fragment: location.fragment
fragment: location.fragment,
rangeAnchor: location.rangeAnchor
)
}
@@ -1407,7 +1446,8 @@ extension RDEPUBReaderController {
href: location.href,
progression: location.progression,
lastProgression: location.lastProgression,
fragment: location.fragment
fragment: location.fragment,
rangeAnchor: location.rangeAnchor
)
}
@@ -1499,21 +1539,32 @@ extension RDEPUBReaderController {
href: searchMatch.href,
progression: searchMatch.progression,
lastProgression: searchMatch.progression,
fragment: nil
fragment: nil,
rangeAnchor: searchMatch.rangeAnchor
)
return restoreReadingLocation(location, animated: animated)
}
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
if let chapterData = textChapterData(forNormalizedHref: searchMatch.href) {
let location = RDEPUBLocation(
bookIdentifier: currentBookIdentifier,
href: chapterData.href,
progression: searchMatch.progression,
lastProgression: searchMatch.progression,
fragment: nil,
rangeAnchor: searchMatch.rangeAnchor
)
if let pageNumber = chapterData.pageNumber(for: location) {
return pageNumber
}
if let rangeLocation = searchMatch.rangeLocation,
let page = chapterData.pages.first(where: {
rangeLocation >= $0.pageStartOffset && rangeLocation <= $0.pageEndOffset
}) {
return page.absolutePageIndex + 1
}
}
let location = RDEPUBLocation(
@@ -1521,7 +1572,8 @@ extension RDEPUBReaderController {
href: searchMatch.href,
progression: searchMatch.progression,
lastProgression: searchMatch.progression,
fragment: nil
fragment: nil,
rangeAnchor: searchMatch.rangeAnchor
)
if let textBook, let publication {
@@ -1554,9 +1606,12 @@ extension RDEPUBReaderController {
}
let currentMatch = searchState.currentMatch
let normalizedCurrentHref = currentMatch.map { publication.resourceResolver.normalizedHref($0.href) ?? $0.href }
let resources = pageHrefs.map { href in
let matchCount = searchState.matches.filter { $0.href == href }.count
let activeLocalMatchIndex = currentMatch?.href == href ? currentMatch?.localMatchIndex : nil
let matchCount = searchState.matches.filter {
(publication.resourceResolver.normalizedHref($0.href) ?? $0.href) == href
}.count
let activeLocalMatchIndex = normalizedCurrentHref == href ? currentMatch?.localMatchIndex : nil
return RDEPUBSearchPresentationResource(
href: href,
matchCount: matchCount,
@@ -1611,6 +1666,11 @@ extension RDEPUBReaderController: RDReaderDataSource, RDReaderDelegate {
}
private func textHighlights(for page: RDEPUBTextPage) -> [RDEPUBHighlight] {
if let textBook,
let chapterData = textBook.chapterData(for: page.href) {
return chapterData.highlights(on: page, from: activeHighlights)
}
guard let publication else {
return activeHighlights.filter { $0.location.href == page.href }
}
@@ -1620,6 +1680,14 @@ extension RDEPUBReaderController: RDReaderDataSource, RDReaderDelegate {
}
}
private func textChapterData(forNormalizedHref href: String) -> RDEPUBChapterData? {
guard let textBook, let publication else { return nil }
let normalizedHref = publication.resourceResolver.normalizedHref(href) ?? href
return textBook.chapters.lazy
.first(where: { (publication.resourceResolver.normalizedHref($0.href) ?? $0.href) == normalizedHref })
.flatMap { textBook.chapterData(for: $0.href) }
}
public func topToolView(readerView: RDReaderView) -> UIView? {
topToolView
}
@@ -1728,27 +1796,22 @@ extension RDEPUBReaderController: RDEPUBTextContentViewDelegate {
private func normalizedTextSelection(_ selection: RDEPUBSelection) -> RDEPUBSelection? {
guard let textBook,
let chapter = textBook.chapters.first(where: { $0.href == selection.location.href }) else {
let chapterData = textBook.chapterData(for: selection.location.href) else {
return scopedSelection(selection, relativeToSpineIndex: nil)
}
guard let payload = RDEPUBTextOffsetRangeInfo.decode(from: selection.rangeInfo) else {
return scopedSelection(selection, relativeToSpineIndex: nil)
}
let contentLength = max(chapter.attributedContent.length, 1)
let contentLength = max(chapterData.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))
let absoluteRange = NSRange(location: start, length: endExclusive - start)
let location = chapterData.location(for: absoluteRange, bookIdentifier: currentBookIdentifier)
return RDEPUBSelection(
bookIdentifier: currentBookIdentifier,
location: RDEPUBLocation(
bookIdentifier: currentBookIdentifier,
href: selection.location.href,
progression: Double(start) / Double(lastInclusiveOffset),
lastProgression: Double(lastSelectedOffset) / Double(lastInclusiveOffset),
fragment: nil
),
location: location,
text: selection.text,
rangeInfo: selection.rangeInfo,
createdAt: selection.createdAt
@@ -1757,6 +1820,13 @@ extension RDEPUBReaderController: RDEPUBTextContentViewDelegate {
private func pageNumber(for location: RDEPUBLocation) -> Int? {
if let textBook, let publication {
// Anchor-based lookup (character-level precision)
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,
@@ -1779,13 +1849,14 @@ extension RDEPUBReaderController: RDEPUBTextContentViewDelegate {
private func resolvedTextLocation(forPageNumber pageNumber: Int) -> RDEPUBLocation? {
guard let textBook,
let publication,
let location = textBook.location(
forPageNumber: pageNumber,
bookIdentifier: currentBookIdentifier
) else {
let page = textBook.page(at: pageNumber) else {
return nil
}
let location = textBook.chapterData(for: page.href)?.location(forPage: page, bookIdentifier: currentBookIdentifier)
?? textBook.location(forPageNumber: pageNumber, bookIdentifier: currentBookIdentifier)
guard let location else { return nil }
return publication.resourceResolver.normalizedLocation(
location,
relativeToSpineIndex: nil,