704 lines
24 KiB
Swift
704 lines
24 KiB
Swift
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 handleHighlightMenuAction(_ action: RDEPUBExistingHighlightMenuAction, highlight: RDEPUBHighlight) {
|
|
annotationCoordinator.handleHighlightMenuAction(action, highlight: highlight)
|
|
}
|
|
|
|
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()
|
|
}
|
|
}
|