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) } }