177 lines
6.8 KiB
Swift
177 lines
6.8 KiB
Swift
import UIKit
|
||
|
||
// MARK: - Web 内容视图代理(EPUB 固定布局/Web 渲染路径)
|
||
|
||
extension RDEPUBReaderController: RDEPUBWebContentViewDelegate {
|
||
func epubWebContentView(_ contentView: RDEPUBWebContentView, didUpdateLocation location: RDEPUBLocation, spineIndex: Int) {
|
||
guard readerView.currentPage >= 0,
|
||
activePages.indices.contains(readerView.currentPage) else {
|
||
return
|
||
}
|
||
|
||
let currentPage = activePages[readerView.currentPage]
|
||
guard readingSession?.pageContains(spineIndex: spineIndex, in: currentPage) == true else {
|
||
return
|
||
}
|
||
|
||
persist(location: location)
|
||
readingSession?.updateReadingContext(
|
||
pageNumber: readerView.currentPage + 1,
|
||
location: location,
|
||
spineIndex: spineIndex,
|
||
chapterIndex: currentPage.chapterIndex,
|
||
bookIdentifier: currentBookIdentifier
|
||
)
|
||
}
|
||
|
||
func epubWebContentView(_ contentView: RDEPUBWebContentView, didChangeSelection selection: RDEPUBSelection?, spineIndex: Int) {
|
||
if let selection {
|
||
updateCurrentSelection(scopedSelection(selection, relativeToSpineIndex: spineIndex))
|
||
} else {
|
||
updateCurrentSelection(nil)
|
||
}
|
||
}
|
||
|
||
func epubWebContentView(_ contentView: RDEPUBWebContentView, didRequestSelectionAction action: RDEPUBAnnotationMenuAction) {
|
||
handleSelectionMenuAction(action, selection: currentSelection)
|
||
}
|
||
|
||
func epubWebContentView(_ contentView: RDEPUBWebContentView, didActivateInternalLink location: RDEPUBLocation, fromSpineIndex: Int) {
|
||
guard let readingSession,
|
||
let pageNumber = readingSession.queueNavigation(
|
||
to: location,
|
||
relativeToSpineIndex: fromSpineIndex,
|
||
bookIdentifier: currentBookIdentifier
|
||
) else {
|
||
return
|
||
}
|
||
readerView.transitionToPage(pageNum: max(pageNumber - 1, 0), animated: true)
|
||
}
|
||
|
||
func epubWebContentView(_ contentView: RDEPUBWebContentView, didActivateExternalLink url: URL) {
|
||
delegate?.epubReader(self, didActivateExternalLink: url)
|
||
UIApplication.shared.open(url, options: [:], completionHandler: nil)
|
||
}
|
||
|
||
func epubWebContentView(_ contentView: RDEPUBWebContentView, didLogJavaScriptError message: String) {
|
||
print("EPUB JS Error: \(message)")
|
||
}
|
||
}
|
||
|
||
// MARK: - 文本内容视图代理(Native Text 渲染路径)
|
||
|
||
extension RDEPUBReaderController: RDEPUBTextContentViewDelegate {
|
||
func textContentView(_ contentView: RDEPUBTextContentView, didChangeSelection selection: RDEPUBSelection?) {
|
||
guard let selection else {
|
||
updateCurrentSelection(nil)
|
||
return
|
||
}
|
||
updateCurrentSelection(normalizedTextSelection(selection))
|
||
}
|
||
|
||
func textContentView(_ contentView: RDEPUBTextContentView, didRequestSelectionAction action: RDEPUBAnnotationMenuAction) {
|
||
handleSelectionMenuAction(action, selection: currentSelection)
|
||
contentView.clearSelection()
|
||
}
|
||
|
||
private func normalizedTextSelection(_ selection: RDEPUBSelection) -> RDEPUBSelection? {
|
||
guard let textBook,
|
||
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(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 absoluteRange = NSRange(location: start, length: endExclusive - start)
|
||
let location = chapterData.location(for: absoluteRange, bookIdentifier: currentBookIdentifier)
|
||
return RDEPUBSelection(
|
||
bookIdentifier: currentBookIdentifier,
|
||
location: location,
|
||
text: selection.text,
|
||
rangeInfo: selection.rangeInfo,
|
||
createdAt: selection.createdAt
|
||
)
|
||
}
|
||
|
||
func pageNumber(for location: RDEPUBLocation) -> Int? {
|
||
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? {
|
||
guard let textBook,
|
||
let publication,
|
||
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 }
|
||
|
||
return publication.resourceResolver.normalizedLocation(
|
||
location,
|
||
relativeToSpineIndex: nil,
|
||
bookIdentifier: currentBookIdentifier
|
||
) ?? location
|
||
}
|
||
|
||
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
|
||
)
|
||
}
|
||
|
||
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)
|
||
}
|
||
}
|