import UIKit final class RDEPUBReaderContext { private let activityLock = NSLock() private var lastUserNavigationTimestamp: CFAbsoluteTime = 0 weak var controller: RDEPUBReaderController? weak var readerView: RDReaderView? let state: RDEPUBReaderState let environment: RDEPUBReaderEnvironment let services: RDEPUBReaderServices var dependencies: RDEPUBReaderDependencies { get { services.dependencies } set { services.dependencies = newValue environment.displayEnvironment = newValue.environment } } var runtime: RDEPUBReaderRuntime? { controller?.runtime } var parser: RDEPUBParser? { get { state.parser } set { state.parser = newValue } } var publication: RDEPUBPublication? { get { state.publication } set { state.publication = newValue } } var readingSession: RDEPUBReadingSession? { get { state.readingSession } set { state.readingSession = newValue } } var textBook: RDEPUBTextBook? { get { state.textBook } set { state.textBook = newValue } } var bookPageMap: RDEPUBBookPageMap? { get { state.bookPageMap } set { state.bookPageMap = newValue } } var activeBookmarks: [RDEPUBBookmark] { get { state.activeBookmarks } set { state.activeBookmarks = newValue } } var activeHighlights: [RDEPUBHighlight] { get { state.activeHighlights } set { state.activeHighlights = newValue } } var currentBookIdentifier: String? { get { state.currentBookIdentifier } set { state.currentBookIdentifier = newValue } } var paginationToken: UUID { get { state.paginationToken } set { state.paginationToken = newValue } } var paginator: RDEPUBPaginator? { get { state.paginator } set { state.paginator = newValue } } var searchState: RDEPUBSearchState? { get { state.searchState } set { state.searchState = newValue } } var pendingFullPageMap: RDEPUBBookPageMap? { get { state.pendingFullPageMap } set { state.pendingFullPageMap = newValue } } var lastTextPaginationPageSize: CGSize? { get { state.lastTextPaginationPageSize } set { state.lastTextPaginationPageSize = newValue } } var lastMetadataParseWallClockMs: Int { get { state.lastMetadataParseWallClockMs } set { state.lastMetadataParseWallClockMs = newValue } } var lastMetadataParseConcurrency: Int { get { state.lastMetadataParseConcurrency } set { state.lastMetadataParseConcurrency = newValue } } var currentSelection: RDEPUBSelection? { get { state.currentSelection } set { state.currentSelection = newValue } } var selectionState: RDEPUBSelectionState { get { state.selectionState } set { state.selectionState = newValue } } var configuration: RDEPUBReaderConfiguration = .default var persistence: RDEPUBReaderPersistence? var epubURL: URL = URL(string: "about:blank")! var isRepaginating: Bool { get { state.isRepaginating } set { state.isRepaginating = newValue } } var didStartInitialLoad: Bool { get { state.didStartInitialLoad } set { state.didStartInitialLoad = newValue } } var isExternalTextBook: Bool { get { state.isExternalTextBook } set { state.isExternalTextBook = newValue } } var textFileURL: URL? { get { state.textFileURL } set { state.textFileURL = newValue } } var textBookCache: RDEPUBTextBookCache { state.textBookCache } init(controller: RDEPUBReaderController) { self.controller = controller self.readerView = controller.readerView let state = RDEPUBReaderState() self.state = state self.environment = RDEPUBReaderEnvironment( controller: controller, readerView: controller.readerView, displayEnvironment: RDEPUBUIScreenEnvironment() ) self.services = RDEPUBReaderServices(dependencies: .live) } func currentLayoutContext() -> RDEPUBNavigatorLayoutContext { environment.currentLayoutContext(configuration: configuration) } func currentPreferences() -> RDEPUBPreferences { configuration.makePreferences() } func currentTextPageSize() -> CGSize { if Thread.isMainThread { let pageNum = (readerView?.currentPage ?? -1) >= 0 ? readerView?.currentPage : nil if let readerView, let pageNum { let resolvedSize = readerView.resolvedSinglePageSize(pageNum: pageNum) if resolvedSize.width > 0, resolvedSize.height > 0 { return resolvedSize } } let viewportSize = currentLayoutContext().viewportSize if viewportSize.width > 0, viewportSize.height > 0 { return viewportSize } } else if let lastTextPaginationPageSize, lastTextPaginationPageSize.width > 0, lastTextPaginationPageSize.height > 0 { return lastTextPaginationPageSize } else { let mainThreadSize = DispatchQueue.main.sync { [weak self] in self?.currentTextPageSize() ?? .zero } if mainThreadSize.width > 0, mainThreadSize.height > 0 { return mainThreadSize } } return environment.fallbackViewportSize } func currentTextRenderStyle() -> RDEPUBTextRenderStyle { environment.currentTextRenderStyle(configuration: configuration) } func currentTextLayoutConfig(pageSize: CGSize) -> RDEPUBTextLayoutConfig { environment.currentTextLayoutConfig(configuration: configuration, pageSize: pageSize) } func resolvedTextRenderer() -> RDEPUBTextRenderer { services.resolvedTextRenderer(configuration: configuration) } var activePages: [EPUBPage] { state.activePages } var activeChapters: [EPUBChapterInfo] { state.activeChapters } var currentBrightness: CGFloat { get { environment.currentBrightness } set { environment.currentBrightness = newValue } } func replaceActiveSnapshot(_ snapshot: RDEPUBReadingSession.PaginationSnapshot) { state.replaceActiveSnapshot(snapshot) } func clearActiveSnapshot() { state.clearActiveSnapshot() } func makeParser() -> RDEPUBParser { services.makeParser() } func makePaginator() -> RDEPUBPaginator { services.makePaginator() } func makeTextBookBuilder(layoutConfig: RDEPUBTextLayoutConfig) -> RDEPUBTextBookBuilder { services.makeTextBookBuilder( configuration: configuration, cache: textBookCache, layoutConfig: layoutConfig ) } func makeChapterSummaryDiskCache() -> RDEPUBChapterSummaryDiskCache { services.makeChapterSummaryDiskCache(bookIdentifier: currentBookIdentifier) } func chapterCacheKey(forSpineIndex spineIndex: Int) -> RDEPUBChapterCacheKey { let contentHash: String if let parser, let publication, publication.spine.indices.contains(spineIndex) { let href = publication.spine[spineIndex].href contentHash = parser.htmlString(forRelativePath: href)?.sha256Hex ?? "" } else { contentHash = "" } return chapterCacheKey( forSpineIndex: spineIndex, precomputedContentHash: contentHash, renderSignature: currentRenderSignature() ) } func chapterCacheKey(forSpineIndex spineIndex: Int, precomputedContentHash: String) -> RDEPUBChapterCacheKey { chapterCacheKey( forSpineIndex: spineIndex, precomputedContentHash: precomputedContentHash, renderSignature: currentRenderSignature() ) } func chapterCacheKey( forSpineIndex spineIndex: Int, precomputedContentHash: String, renderSignature: String ) -> RDEPUBChapterCacheKey { RDEPUBChapterCacheKey( bookID: currentBookIdentifier ?? "", spineIndex: spineIndex, renderSignature: renderSignature, chapterContentHash: precomputedContentHash ) } func currentRenderSignature() -> String { let style = currentTextRenderStyle() let pageSize = currentTextPageSize() let layoutConfig = currentTextLayoutConfig(pageSize: pageSize) return [ style.font.fontName, "\(style.font.pointSize)", "\(configuration.lineHeightMultiple)", "\(style.lineSpacing)", layoutConfig.cacheSignature, "\(RDEPUBChapterSummary.currentSchemaVersion)" ].joined(separator: "|") } func chapterSummary(forSpineIndex spineIndex: Int) -> RDEPUBChapterSummary? { runtime?.summaryDiskCache.read(for: chapterCacheKey(forSpineIndex: spineIndex)) } func normalizedSpineIndex(for location: RDEPUBLocation) -> Int? { guard let publication else { return nil } let normalizedLocation = publication.resourceResolver.normalizedLocation( location, relativeToSpineIndex: nil, bookIdentifier: currentBookIdentifier ) ?? location guard let normalizedHref = publication.resourceResolver.normalizedHref(normalizedLocation.href) else { return nil } return publication.spine.firstIndex { publication.resourceResolver.normalizedHref($0.href) == normalizedHref } } func makePlainTextBookBuilder(layoutConfig: RDEPUBTextLayoutConfig) -> RDPlainTextBookBuilder { services.makePlainTextBookBuilder( configuration: configuration, layoutConfig: layoutConfig ) } func currentVisibleLocation() -> RDEPUBLocation? { controller?.currentVisibleLocation() } func persistenceLocation() -> RDEPUBLocation? { guard let currentBookIdentifier else { return nil } return persistence?.loadLocation(for: currentBookIdentifier) } func persist(location: RDEPUBLocation) { guard let currentBookIdentifier else { return } persistence?.saveLocation(location, for: currentBookIdentifier) } func markUserNavigationActivity() { activityLock.lock() lastUserNavigationTimestamp = CFAbsoluteTimeGetCurrent() activityLock.unlock() } func secondsSinceLastUserNavigation() -> CFAbsoluteTime { activityLock.lock() let timestamp = lastUserNavigationTimestamp activityLock.unlock() guard timestamp > 0 else { return .greatestFiniteMagnitude } return CFAbsoluteTimeGetCurrent() - timestamp } 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) } } func showLoading() { controller?.showLoading() } func hideLoading() { controller?.hideLoading() } func handle(error: Error) { controller?.handle(error: error) } func updateReaderChrome() { controller?.updateReaderChrome() } func refreshVisibleContentPreservingLocation() { controller?.refreshVisibleContentPreservingLocation() } func restoreReadingLocation(_ location: RDEPUBLocation, animated: Bool = false) -> Bool { controller?.restoreReadingLocation(location, animated: animated) ?? false } func repaginatePreservingCurrentLocation() { controller?.repaginatePreservingCurrentLocation() } func applyReaderViewConfiguration() { controller?.applyReaderViewConfiguration() } }