refactor: rename RDReaderView -> RDEpubReaderView, update pod config and docs
- Rename source module from RDReaderView to RDEpubReaderView - Move all source files from Sources/RDReaderView/ to Sources/RDEpubReaderView/ - Update podspec: RDReaderView.podspec -> RDEpubReaderView.podspec - Update Podfile, demo project, and CocoaPods config for new pod name - Delete old RDReaderView pod support files from ReadViewDemo/Pods - Add new RDEpubReaderView pod support files - Update documentation (API ref, architecture, UML, conventions, etc.) - Add FixedLayoutRotationTests - Update .gitignore: exclude .DS_Store, manual unpack backups, _ssoft-output
This commit is contained in:
@@ -0,0 +1,703 @@
|
||||
import UIKit
|
||||
|
||||
final class RDEPUBReaderRuntime {
|
||||
|
||||
private unowned let context: RDEPUBReaderContext
|
||||
|
||||
lazy var chapterRuntimeStore = RDEPUBChapterRuntimeStore()
|
||||
|
||||
lazy var summaryDiskCache = context.makeChapterSummaryDiskCache()
|
||||
|
||||
lazy var chapterLoader: RDEPUBChapterLoader = {
|
||||
let loader = RDEPUBChapterLoader(context: context)
|
||||
loader.setSummaryDiskCache(summaryDiskCache)
|
||||
loader.onDeferredCFIMapReady = { [weak self] spineIndex in
|
||||
self?.handleDeferredCFIMapReady(for: spineIndex)
|
||||
}
|
||||
return loader
|
||||
}()
|
||||
|
||||
lazy var pageResolver = RDEPUBPageResolver(context: context, store: chapterRuntimeStore)
|
||||
|
||||
lazy var loadCoordinator = RDEPUBReaderLoadCoordinator(context: context)
|
||||
|
||||
lazy var paginationCoordinator = RDEPUBReaderPaginationCoordinator(context: context)
|
||||
|
||||
lazy var locationCoordinator = RDEPUBReaderLocationCoordinator(context: context)
|
||||
|
||||
lazy var searchCoordinator = RDEPUBReaderSearchCoordinator(context: context)
|
||||
|
||||
lazy var chromeCoordinator = RDEPUBReaderChromeCoordinator(context: context)
|
||||
|
||||
lazy var annotationCoordinator = RDEPUBReaderAnnotationCoordinator(context: context)
|
||||
|
||||
lazy var viewportMonitor = RDEPUBReaderViewportMonitor(context: context)
|
||||
|
||||
lazy var jumpSessionManager = RDEPUBJumpSessionManager(context: context)
|
||||
|
||||
lazy var backgroundPriorityManager = RDEPUBBackgroundPriorityManager(context: context)
|
||||
|
||||
lazy var backgroundCoverageStore = RDEPUBBackgroundCoverageStore(context: context)
|
||||
|
||||
lazy var reconciliationCoordinator = RDEPUBPageMapReconciliationCoordinator(context: context)
|
||||
|
||||
lazy var presentationRuntime = RDEPUBPresentationRuntime(
|
||||
context: context,
|
||||
locationCoordinator: locationCoordinator,
|
||||
jumpSessionManager: jumpSessionManager,
|
||||
reconciliationCoordinator: reconciliationCoordinator
|
||||
)
|
||||
|
||||
lazy var chapterWarmupOrchestrator = RDEPUBChapterWarmupOrchestrator(
|
||||
context: context,
|
||||
store: chapterRuntimeStore,
|
||||
loader: chapterLoader,
|
||||
presentationRuntime: presentationRuntime,
|
||||
locationCoordinator: locationCoordinator,
|
||||
backgroundPriorityManager: backgroundPriorityManager,
|
||||
jumpSessionManager: jumpSessionManager,
|
||||
refreshVisibleContentPreservingLocation: { [weak self] in
|
||||
self?.refreshVisibleContentPreservingLocation()
|
||||
}
|
||||
)
|
||||
|
||||
var isSettingsPanelOpen: Bool = false
|
||||
|
||||
var needsFullRepaginationAfterSettingsClose: Bool = false
|
||||
|
||||
private var settingsPreviewGeneration: Int = 0
|
||||
|
||||
private var pendingSettingsPreviewWorkItem: DispatchWorkItem?
|
||||
|
||||
/// The location before the settings preview replaces the full book page map
|
||||
/// with the current chapter's temporary map. The temporary map rebases that
|
||||
/// chapter at page zero, so it must never be used as the final restore source.
|
||||
private var settingsRestoreLocation: RDEPUBLocation?
|
||||
|
||||
/// A settings session must keep one immutable text offset. Re-capturing the
|
||||
/// start of each preview page makes repeated font-size changes drift backward.
|
||||
private var settingsPreviewAnchor: SettingsPreviewAnchor?
|
||||
|
||||
private let settingsPreviewDebounceDelay: TimeInterval = 0.2
|
||||
|
||||
private struct SettingsPreviewAnchor {
|
||||
|
||||
let spineIndex: Int
|
||||
|
||||
let href: String
|
||||
|
||||
let offset: Int
|
||||
}
|
||||
|
||||
init(context: RDEPUBReaderContext) {
|
||||
self.context = context
|
||||
}
|
||||
|
||||
func makeTopToolView() -> RDEPUBReaderTopToolViewProviding {
|
||||
chromeCoordinator.makeTopToolView()
|
||||
}
|
||||
|
||||
func makeBottomToolView() -> RDEPUBReaderBottomToolViewProviding {
|
||||
chromeCoordinator.makeBottomToolView()
|
||||
}
|
||||
|
||||
func startInitialLoadIfNeeded() {
|
||||
loadCoordinator.startInitialLoadIfNeeded()
|
||||
}
|
||||
|
||||
func reloadBook() {
|
||||
guard let readerView = context.readerView else { return }
|
||||
context.didStartInitialLoad = false
|
||||
context.parser = nil
|
||||
context.publication = nil
|
||||
context.clearActiveSnapshot()
|
||||
context.readingSession = nil
|
||||
context.textBook = nil
|
||||
context.bookPageMap = nil
|
||||
context.pendingPageMapUpdates.removeAll()
|
||||
context.activeBookmarks = []
|
||||
context.activeHighlights = []
|
||||
context.searchState = nil
|
||||
clearOnDemandPageModeState()
|
||||
viewportMonitor.resetForReload()
|
||||
annotationCoordinator.updateCurrentSelection(nil)
|
||||
readerView.reloadData()
|
||||
startInitialLoadIfNeeded()
|
||||
}
|
||||
|
||||
func go(to location: RDEPUBLocation, animated: Bool = false) -> Bool {
|
||||
locationCoordinator.restoreReadingLocation(location, animated: animated)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func go(toPageNumber pageNumber: Int, animated: Bool = false) -> Bool {
|
||||
guard context.controller != nil,
|
||||
let readerView = context.readerView,
|
||||
pageNumber > 0 else {
|
||||
return false
|
||||
}
|
||||
|
||||
if let textBook = context.textBook {
|
||||
guard textBook.page(at: pageNumber) != nil else {
|
||||
return false
|
||||
}
|
||||
readerView.transitionToPage(pageNum: pageNumber - 1, animated: animated)
|
||||
if let location = locationCoordinator.currentVisibleLocation() {
|
||||
context.persist(location: location)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
if context.bookPageMap != nil {
|
||||
guard prepareOnDemandChapter(forAbsolutePageNumber: pageNumber) else {
|
||||
return false
|
||||
}
|
||||
readerView.transitionToPage(pageNum: pageNumber - 1, animated: animated)
|
||||
if let location = locationCoordinator.currentVisibleLocation() {
|
||||
context.persist(location: location)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
guard context.activePages.indices.contains(pageNumber - 1) else {
|
||||
return false
|
||||
}
|
||||
readerView.transitionToPage(pageNum: pageNumber - 1, animated: animated)
|
||||
if let location = locationCoordinator.currentVisibleLocation() {
|
||||
context.persist(location: location)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func clearSelection() {
|
||||
annotationCoordinator.updateCurrentSelection(nil)
|
||||
}
|
||||
|
||||
func bookmark(withID id: String) -> RDEPUBBookmark? {
|
||||
annotationCoordinator.bookmark(withID: id)
|
||||
}
|
||||
|
||||
func highlight(withID id: String) -> RDEPUBHighlight? {
|
||||
annotationCoordinator.highlight(withID: id)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func addHighlight(
|
||||
from selection: RDEPUBSelection? = nil,
|
||||
color: String = "#F8E16C",
|
||||
note: String? = nil
|
||||
) -> RDEPUBHighlight? {
|
||||
annotationCoordinator.addHighlight(from: selection, color: color, note: note)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func addAnnotation(
|
||||
from selection: RDEPUBSelection? = nil,
|
||||
style: RDEPUBHighlightStyle,
|
||||
color: String = "#F8E16C",
|
||||
note: String? = nil
|
||||
) -> RDEPUBHighlight? {
|
||||
annotationCoordinator.addAnnotation(from: selection, style: style, color: color, note: note)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func upsertHighlight(_ highlight: RDEPUBHighlight) -> RDEPUBHighlight? {
|
||||
annotationCoordinator.upsertHighlight(highlight)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func removeHighlight(id: String) -> RDEPUBHighlight? {
|
||||
annotationCoordinator.removeHighlight(id: id)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func updateHighlightNote(id: String, note: String?) -> RDEPUBHighlight? {
|
||||
annotationCoordinator.updateHighlightNote(id: id, note: note)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func go(toHighlightID id: String, animated: Bool = true) -> Bool {
|
||||
annotationCoordinator.go(toHighlightID: id, animated: animated)
|
||||
}
|
||||
|
||||
func removeAllHighlights() {
|
||||
annotationCoordinator.removeAllHighlights()
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func addBookmark(note: String? = nil) -> RDEPUBBookmark? {
|
||||
annotationCoordinator.addBookmark(note: note)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func toggleBookmark(note: String? = nil) -> RDEPUBBookmark? {
|
||||
annotationCoordinator.toggleBookmark(note: note)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func removeBookmark(id: String) -> RDEPUBBookmark? {
|
||||
annotationCoordinator.removeBookmark(id: id)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func go(toBookmarkID id: String, animated: Bool = true) -> Bool {
|
||||
annotationCoordinator.go(toBookmarkID: id, animated: animated)
|
||||
}
|
||||
|
||||
func presentBookmarksManager() {
|
||||
annotationCoordinator.presentBookmarksManager()
|
||||
}
|
||||
|
||||
func presentHighlightsManager() {
|
||||
annotationCoordinator.presentHighlightsManager()
|
||||
}
|
||||
|
||||
func presentAnnotationCreation() {
|
||||
annotationCoordinator.presentAnnotationCreation()
|
||||
}
|
||||
|
||||
func presentHighlightActions(for highlight: RDEPUBHighlight, sourceView: UIView, sourceRect: CGRect) {
|
||||
annotationCoordinator.presentHighlightActions(for: highlight, sourceView: sourceView, sourceRect: sourceRect)
|
||||
}
|
||||
|
||||
func handleSelectionMenuAction(_ action: RDEPUBAnnotationMenuAction, selection: RDEPUBSelection?) {
|
||||
annotationCoordinator.handleSelectionMenuAction(action, selection: selection)
|
||||
}
|
||||
|
||||
func search(keyword: String) {
|
||||
searchCoordinator.search(keyword: keyword)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func searchNext() -> Bool {
|
||||
searchCoordinator.searchNext()
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func searchPrevious() -> Bool {
|
||||
searchCoordinator.searchPrevious()
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func selectSearchMatch(at index: Int) -> Bool {
|
||||
searchCoordinator.selectSearchMatch(at: index)
|
||||
}
|
||||
|
||||
func clearSearch() {
|
||||
searchCoordinator.clearSearch()
|
||||
}
|
||||
|
||||
func searchPresentation(for page: EPUBPage) -> RDEPUBSearchPresentation? {
|
||||
searchCoordinator.searchPresentation(for: page)
|
||||
}
|
||||
|
||||
func updateReaderChrome() {
|
||||
chromeCoordinator.updateReaderChrome()
|
||||
}
|
||||
|
||||
func presentSettings() {
|
||||
chromeCoordinator.presentSettings()
|
||||
}
|
||||
|
||||
func presentTableOfContents() {
|
||||
chromeCoordinator.presentTableOfContents()
|
||||
}
|
||||
|
||||
func handleBackAction() {
|
||||
chromeCoordinator.handleBackAction()
|
||||
}
|
||||
|
||||
func loadPublication() {
|
||||
loadCoordinator.loadPublication()
|
||||
}
|
||||
|
||||
func applyParsedPublication(
|
||||
parser: RDEPUBParser,
|
||||
publication: RDEPUBPublication,
|
||||
bookIdentifier: String,
|
||||
restoreLocation: RDEPUBLocation?,
|
||||
bookmarks: [RDEPUBBookmark],
|
||||
highlights: [RDEPUBHighlight]
|
||||
) {
|
||||
loadCoordinator.applyParsedPublication(
|
||||
parser: parser,
|
||||
publication: publication,
|
||||
bookIdentifier: bookIdentifier,
|
||||
restoreLocation: restoreLocation,
|
||||
bookmarks: bookmarks,
|
||||
highlights: highlights
|
||||
)
|
||||
}
|
||||
|
||||
func paginatePublication(restoreLocation: RDEPUBLocation?) {
|
||||
paginationCoordinator.paginatePublication(restoreLocation: restoreLocation)
|
||||
}
|
||||
|
||||
func applyTextBook(_ textBook: RDEPUBTextBook, restoreLocation: RDEPUBLocation?) {
|
||||
paginationCoordinator.applyTextBook(textBook, restoreLocation: restoreLocation)
|
||||
}
|
||||
|
||||
func applyPaginationSnapshot(
|
||||
_ snapshot: (pages: [EPUBPage], chapters: [EPUBChapterInfo]),
|
||||
restoreLocation: RDEPUBLocation?
|
||||
) {
|
||||
paginationCoordinator.applyPaginationSnapshot(snapshot, restoreLocation: restoreLocation)
|
||||
}
|
||||
|
||||
func applyBookPageMap(_ bookPageMap: RDEPUBBookPageMap, restoreLocation: RDEPUBLocation?) {
|
||||
presentationRuntime.applyBookPageMap(
|
||||
bookPageMap,
|
||||
restoreLocation: restoreLocation
|
||||
) { [weak self] restoreLocation in
|
||||
self?.paginationCoordinator.finishPagination(restoreLocation: restoreLocation)
|
||||
}
|
||||
}
|
||||
|
||||
func refreshBookPageMapInPlace(_ bookPageMap: RDEPUBBookPageMap) {
|
||||
presentationRuntime.refreshBookPageMapInPlace(bookPageMap)
|
||||
}
|
||||
|
||||
func applyPendingFullPageMapIfNeeded() {
|
||||
presentationRuntime.commitPendingPageMapUpdateIfNeeded()
|
||||
}
|
||||
|
||||
func finishPagination(restoreLocation: RDEPUBLocation?) {
|
||||
paginationCoordinator.finishPagination(restoreLocation: restoreLocation)
|
||||
}
|
||||
|
||||
func repaginatePreservingCurrentLocation() {
|
||||
|
||||
if isSettingsPanelOpen {
|
||||
needsFullRepaginationAfterSettingsClose = true
|
||||
paginationCoordinator.cancelActiveMetadataParseWork()
|
||||
scheduleSettingsPreviewRepagination()
|
||||
} else {
|
||||
paginationCoordinator.repaginatePreservingCurrentLocation()
|
||||
}
|
||||
}
|
||||
|
||||
private func scheduleSettingsPreviewRepagination() {
|
||||
pendingSettingsPreviewWorkItem?.cancel()
|
||||
settingsPreviewGeneration += 1
|
||||
let previewGeneration = settingsPreviewGeneration
|
||||
|
||||
let workItem = DispatchWorkItem { [weak self] in
|
||||
guard let self,
|
||||
self.isSettingsPanelOpen,
|
||||
previewGeneration == self.settingsPreviewGeneration else {
|
||||
return
|
||||
}
|
||||
self.pendingSettingsPreviewWorkItem = nil
|
||||
let previewAnchor = self.settingsPreviewAnchor
|
||||
?? self.captureSettingsPreviewAnchor()
|
||||
self.chapterRuntimeStore.invalidateAllLayoutDependentContent()
|
||||
self.repaginateCurrentChapterOnly(
|
||||
previewGeneration: previewGeneration,
|
||||
previewAnchor: previewAnchor
|
||||
)
|
||||
}
|
||||
pendingSettingsPreviewWorkItem = workItem
|
||||
DispatchQueue.main.asyncAfter(
|
||||
deadline: .now() + settingsPreviewDebounceDelay,
|
||||
execute: workItem
|
||||
)
|
||||
}
|
||||
|
||||
private func captureSettingsPreviewAnchor() -> SettingsPreviewAnchor? {
|
||||
guard let bookPageMap = context.bookPageMap,
|
||||
let readerView = context.readerView else { return nil }
|
||||
|
||||
let absolutePageIndex = readerView.currentPage
|
||||
guard absolutePageIndex >= 0,
|
||||
let spineIndex = bookPageMap.spineIndex(forAbsolutePage: absolutePageIndex),
|
||||
let localPageIndex = bookPageMap.localPageIndex(forAbsolutePage: absolutePageIndex),
|
||||
let chapter = chapterRuntimeStore.chapterData(for: spineIndex),
|
||||
chapter.pages.indices.contains(localPageIndex) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let page = chapter.pages[localPageIndex]
|
||||
let offset = page.contentRange.length > 0
|
||||
? page.contentRange.location
|
||||
: page.pageStartOffset
|
||||
return SettingsPreviewAnchor(
|
||||
spineIndex: spineIndex,
|
||||
href: chapter.href,
|
||||
offset: offset
|
||||
)
|
||||
}
|
||||
|
||||
private func repaginateCurrentChapterOnly(
|
||||
previewGeneration: Int,
|
||||
previewAnchor: SettingsPreviewAnchor?
|
||||
) {
|
||||
guard let bookPageMap = context.bookPageMap,
|
||||
let readerView = context.readerView else { return }
|
||||
|
||||
let currentPageNumber = readerView.currentPage + 1
|
||||
guard let currentSpineIndex = bookPageMap.spineIndex(forAbsolutePage: currentPageNumber - 1) else {
|
||||
return
|
||||
}
|
||||
let previewLocation = locationCoordinator.currentVisibleLocation() ?? context.persistenceLocation()
|
||||
|
||||
chapterLoader.loadChapter(
|
||||
spineIndex: currentSpineIndex,
|
||||
store: chapterRuntimeStore,
|
||||
priority: .preview
|
||||
) { [weak self] result in
|
||||
guard let self,
|
||||
self.isSettingsPanelOpen,
|
||||
previewGeneration == self.settingsPreviewGeneration,
|
||||
let readerView = self.context.readerView else {
|
||||
return
|
||||
}
|
||||
|
||||
switch result {
|
||||
case .success(let chapter):
|
||||
let partialMap = self.makePartialPageMap(from: [chapter])
|
||||
self.presentationRuntime.applySettingsPreviewPageMap(partialMap)
|
||||
readerView.reloadData()
|
||||
|
||||
if let targetPage = self.settingsPreviewTargetPage(
|
||||
in: chapter,
|
||||
for: previewAnchor
|
||||
) {
|
||||
readerView.transitionToPage(pageNum: targetPage, animated: false)
|
||||
return
|
||||
}
|
||||
|
||||
if let previewLocation,
|
||||
self.locationCoordinator.restoreReadingLocation(previewLocation, animated: false) {
|
||||
return
|
||||
}
|
||||
readerView.transitionToPage(pageNum: 0, animated: false)
|
||||
|
||||
case .failure(let error):
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func settingsPreviewTargetPage(
|
||||
in chapter: RDEPUBRuntimeChapter,
|
||||
for anchor: SettingsPreviewAnchor?
|
||||
) -> Int? {
|
||||
guard let anchor,
|
||||
anchor.spineIndex == chapter.spineIndex,
|
||||
anchor.href == chapter.href,
|
||||
!chapter.pages.isEmpty else {
|
||||
return nil
|
||||
}
|
||||
|
||||
if let exactPage = chapter.pages.first(where: { page in
|
||||
let lowerBound = page.contentRange.location
|
||||
let upperBound = page.contentRange.location + page.contentRange.length
|
||||
if page.contentRange.length == 0 {
|
||||
return anchor.offset == lowerBound
|
||||
}
|
||||
return anchor.offset >= lowerBound && anchor.offset < upperBound
|
||||
}) {
|
||||
return exactPage.pageIndexInChapter
|
||||
}
|
||||
|
||||
if let nextPage = chapter.pages.first(where: { page in
|
||||
page.contentRange.location > anchor.offset
|
||||
}) {
|
||||
return nextPage.pageIndexInChapter
|
||||
}
|
||||
|
||||
return max(chapter.pages.count - 1, 0)
|
||||
}
|
||||
|
||||
func settingsPanelWillAppear() {
|
||||
pendingSettingsPreviewWorkItem?.cancel()
|
||||
pendingSettingsPreviewWorkItem = nil
|
||||
isSettingsPanelOpen = true
|
||||
needsFullRepaginationAfterSettingsClose = false
|
||||
let fallbackLocation = locationCoordinator.currentVisibleLocation()
|
||||
?? context.persistenceLocation()
|
||||
settingsPreviewAnchor = captureSettingsPreviewAnchor()
|
||||
settingsRestoreLocation = exactSettingsRestoreLocation(
|
||||
anchor: settingsPreviewAnchor,
|
||||
fallbackLocation: fallbackLocation
|
||||
)
|
||||
settingsPreviewGeneration += 1
|
||||
}
|
||||
|
||||
func settingsPanelDidDisappear() {
|
||||
pendingSettingsPreviewWorkItem?.cancel()
|
||||
pendingSettingsPreviewWorkItem = nil
|
||||
isSettingsPanelOpen = false
|
||||
settingsPreviewGeneration += 1
|
||||
|
||||
if needsFullRepaginationAfterSettingsClose {
|
||||
needsFullRepaginationAfterSettingsClose = false
|
||||
paginationCoordinator.repaginatePreservingCurrentLocation(
|
||||
preferredRestoreLocation: settingsRestoreLocation
|
||||
)
|
||||
}
|
||||
settingsRestoreLocation = nil
|
||||
settingsPreviewAnchor = nil
|
||||
}
|
||||
|
||||
private func exactSettingsRestoreLocation(
|
||||
anchor: SettingsPreviewAnchor?,
|
||||
fallbackLocation: RDEPUBLocation?
|
||||
) -> RDEPUBLocation? {
|
||||
guard let anchor else { return fallbackLocation }
|
||||
|
||||
let textAnchor = RDEPUBTextAnchor(
|
||||
fileIndex: anchor.spineIndex,
|
||||
row: 0,
|
||||
column: 0,
|
||||
chapterOffset: anchor.offset,
|
||||
fragmentID: fallbackLocation?.fragment
|
||||
)
|
||||
return RDEPUBLocation(
|
||||
bookIdentifier: fallbackLocation?.bookIdentifier ?? context.currentBookIdentifier,
|
||||
href: anchor.href,
|
||||
progression: fallbackLocation?.progression ?? 0,
|
||||
lastProgression: fallbackLocation?.lastProgression,
|
||||
fragment: fallbackLocation?.fragment,
|
||||
rangeAnchor: RDEPUBTextRangeAnchor(start: textAnchor, end: textAnchor)
|
||||
)
|
||||
}
|
||||
|
||||
func refreshVisibleContentPreservingLocation() {
|
||||
paginationCoordinator.refreshVisibleContentPreservingLocation()
|
||||
}
|
||||
|
||||
func rebuildExternalTextBook() {
|
||||
paginationCoordinator.rebuildExternalTextBook()
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func restoreReadingLocation(
|
||||
_ location: RDEPUBLocation,
|
||||
animated: Bool = false,
|
||||
targetHighlightRangeInfo: String? = nil
|
||||
) -> Bool {
|
||||
locationCoordinator.restoreReadingLocation(
|
||||
location,
|
||||
animated: animated,
|
||||
targetHighlightRangeInfo: targetHighlightRangeInfo
|
||||
)
|
||||
}
|
||||
|
||||
func currentVisibleLocation() -> RDEPUBLocation? {
|
||||
locationCoordinator.currentVisibleLocation()
|
||||
}
|
||||
|
||||
func currentViewportSignature() -> RDEPUBViewportSignature? {
|
||||
viewportMonitor.currentViewportSignature()
|
||||
}
|
||||
|
||||
func handleViewportChangeIfNeeded(
|
||||
reason: RDEPUBViewportChangeReason,
|
||||
viewportSignature: RDEPUBViewportSignature? = nil
|
||||
) {
|
||||
viewportMonitor.handleViewportChangeIfNeeded(reason: reason, viewportSignature: viewportSignature)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func ensureOnDemandNavigationTargetAvailable(for location: RDEPUBLocation) -> Bool {
|
||||
chapterWarmupOrchestrator.ensureNavigationTargetAvailable(for: location)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func prepareOnDemandChapter(
|
||||
forAbsolutePageNumber pageNumber: Int,
|
||||
allowSynchronousLoad: Bool = true,
|
||||
completion: ((Bool) -> Void)? = nil
|
||||
) -> Bool {
|
||||
chapterWarmupOrchestrator.prepareOnDemandChapter(
|
||||
forAbsolutePageNumber: pageNumber,
|
||||
allowSynchronousLoad: allowSynchronousLoad,
|
||||
completion: completion
|
||||
)
|
||||
}
|
||||
|
||||
func extendPartialBookPageMapIfNeeded(
|
||||
currentPageNumber: Int,
|
||||
minimumTrailingPages: Int = 2,
|
||||
batchChapterCount: Int = 3
|
||||
) {
|
||||
chapterWarmupOrchestrator.extendPartialBookPageMapIfNeeded(
|
||||
currentPageNumber: currentPageNumber,
|
||||
minimumTrailingPages: minimumTrailingPages,
|
||||
batchChapterCount: batchChapterCount
|
||||
)
|
||||
}
|
||||
|
||||
func prefetchForwardChaptersAfterInitialOpen(anchorSpineIndex: Int, totalSpineCount: Int) {
|
||||
chapterWarmupOrchestrator.prefetchForwardChaptersAfterInitialOpen(
|
||||
anchorSpineIndex: anchorSpineIndex,
|
||||
totalSpineCount: totalSpineCount
|
||||
)
|
||||
}
|
||||
|
||||
func clearOnDemandPageModeState() {
|
||||
paginationCoordinator.cancelActiveMetadataParseWork()
|
||||
chapterRuntimeStore.invalidateAllLayoutDependentContent()
|
||||
context.bookPageMap = nil
|
||||
context.pendingPageMapUpdates.removeAll()
|
||||
jumpSessionManager.clearSession()
|
||||
backgroundPriorityManager.reset()
|
||||
backgroundCoverageStore.clearAll()
|
||||
chapterWarmupOrchestrator.clear()
|
||||
}
|
||||
|
||||
/// Clears every runtime value derived from the current viewport before a
|
||||
/// full repagination. The caller must capture the visible location first,
|
||||
/// because chapter/page resolution is intentionally invalid after this.
|
||||
func prepareForFullRepagination() {
|
||||
paginationCoordinator.cancelActiveMetadataParseWork()
|
||||
chapterRuntimeStore.invalidateAllLayoutDependentContent()
|
||||
context.pendingPageMapUpdates.removeAll()
|
||||
jumpSessionManager.clearSession()
|
||||
backgroundPriorityManager.reset()
|
||||
backgroundCoverageStore.clearAll()
|
||||
chapterWarmupOrchestrator.clear()
|
||||
context.controller?.textDisplayCache.removeAll()
|
||||
}
|
||||
|
||||
func handleMemoryWarning() {
|
||||
let currentSpineIndex = locationCoordinator.currentVisibleLocation()
|
||||
.flatMap { context.normalizedSpineIndex(for: $0) }
|
||||
let activeWindowIndices: Set<Int> = if let currentSpineIndex {
|
||||
[currentSpineIndex, currentSpineIndex - 1, currentSpineIndex + 1]
|
||||
} else {
|
||||
[]
|
||||
}
|
||||
let protectedIndices = jumpSessionManager.activeSession?.protectedSpineIndices ?? []
|
||||
|
||||
backgroundCoverageStore.handleMemoryWarning(
|
||||
activeWindowSpineIndices: activeWindowIndices,
|
||||
protectedSpineIndices: protectedIndices
|
||||
)
|
||||
chapterRuntimeStore.handleMemoryWarning()
|
||||
}
|
||||
|
||||
private func makePartialPageMap(from chapters: [RDEPUBRuntimeChapter]) -> RDEPUBBookPageMap {
|
||||
var builder = RDEPUBBookPageMap.Builder()
|
||||
for chapter in chapters {
|
||||
builder.add(
|
||||
spineIndex: chapter.spineIndex,
|
||||
href: chapter.href,
|
||||
title: chapter.title,
|
||||
pageCount: chapter.pages.count,
|
||||
fragmentOffsets: chapter.chapterOffsetMap.fragmentOffsets
|
||||
)
|
||||
}
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
private func handleDeferredCFIMapReady(for spineIndex: Int) {
|
||||
guard let currentLocation = locationCoordinator.currentVisibleLocation(),
|
||||
let visibleSpineIndex = context.normalizedSpineIndex(for: currentLocation),
|
||||
visibleSpineIndex == spineIndex else {
|
||||
return
|
||||
}
|
||||
refreshVisibleContentPreservingLocation()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user