import UIKit /// 阅读器共享状态中心:所有 coordinator 通过 context 访问业务状态和便捷方法。 /// /// context 持有: /// - 业务状态(parser、publication、textBook、pages 等) /// - UI 配置(configuration、brightness) /// - 持久化策略(persistence) /// - 便捷方法(renderStyle、layoutConfig 等) /// - 弱引用 controller(仅用于 UIKit 呈现操作) final class RDEPUBReaderContext { private let activityLock = NSLock() private var lastUserNavigationTimestamp: CFAbsoluteTime = 0 // MARK: - 引用 /// 弱引用阅读器控制器,用于 UIKit 呈现操作。 weak var controller: RDEPUBReaderController? /// 弱引用阅读器视图,用于布局和页面状态查询。 weak var readerView: RDReaderView? /// 依赖注入容器,提供解析器、分页器等工厂方法。 var dependencies: RDEPUBReaderDependencies = .live /// 便捷访问当前控制器的运行时协调器集合。 var runtime: RDEPUBReaderRuntime? { controller?.runtime } // MARK: - 业务状态 /// EPUB 解析器实例。 var parser: RDEPUBParser? /// 解析后的出版物模型。 var publication: RDEPUBPublication? /// 当前阅读会话,管理页面和章节状态。 var readingSession: RDEPUBReadingSession? /// 原生文本排版生成的图书模型(仅文本重排模式)。 /// 章节模式下为 nil,内容通过 ChapterRuntimeStore 访问。 var textBook: RDEPUBTextBook? /// 全书轻量页码映射(章节模式)。约 100KB/1000章,不持有 NSAttributedString。 var bookPageMap: RDEPUBBookPageMap? /// 当前书籍的所有书签。 var activeBookmarks: [RDEPUBBookmark] = [] /// 当前书籍的所有高亮标注。 var activeHighlights: [RDEPUBHighlight] = [] /// 当前打开书籍的唯一标识。 var currentBookIdentifier: String? /// 分页操作令牌,用于取消过期的异步分页任务。 var paginationToken = UUID() /// Web 内容分页计算器。 var paginator: RDEPUBPaginator? /// 全文搜索状态。 var searchState: RDEPUBSearchState? /// 后台解析完成的完整 BookPageMap,等待用户下次导航时应用。 /// 避免后台解析完成时直接替换 map 导致当前阅读位置跳转。 var pendingFullPageMap: RDEPUBBookPageMap? /// 上次文本分页时的页面尺寸,用于检测是否需要重新分页。 var lastTextPaginationPageSize: CGSize? /// 后台元数据解析耗时(毫秒),仅包含 OperationQueue 并行阶段。 var lastMetadataParseWallClockMs: Int = 0 /// 后台元数据解析使用的并发数。 var lastMetadataParseConcurrency: Int = 0 /// 当前用户文本选区。 var currentSelection: RDEPUBSelection? // MARK: - 控制器状态(从 controller 下沉) /// 阅读器配置(字号、字体、主题等)。 var configuration: RDEPUBReaderConfiguration = .default /// 持久化策略,负责书签、高亮、阅读位置的存取。 var persistence: RDEPUBReaderPersistence? /// 当前打开的 EPUB 文件 URL。 var epubURL: URL = URL(string: "about:blank")! /// 是否正在重新分页。 var isRepaginating: Bool = false /// 是否已完成首次加载。 var didStartInitialLoad: Bool = false /// 是否为外部传入的纯文本图书。 var isExternalTextBook: Bool = false /// 外部纯文本文件的 URL。 var textFileURL: URL? /// 文本图书缓存,避免重复排版。 var textBookCache = RDEPUBTextBookCache() // MARK: - 初始化 init(controller: RDEPUBReaderController) { self.controller = controller self.readerView = controller.readerView } // MARK: - 便捷方法 /// 根据当前 readerView 和控制器尺寸构建布局上下文。 func currentLayoutContext() -> RDEPUBNavigatorLayoutContext { let containerSize = readerView?.bounds.size ?? .zero let viewSize = controller?.view.bounds.size ?? containerSize let resolvedSize = containerSize == .zero ? viewSize : containerSize return RDEPUBNavigatorLayoutContext( containerSize: resolvedSize, pagesPerScreen: readerView?.pagesPerScreen ?? 1, safeAreaInsets: controller?.view.safeAreaInsets ?? .zero, userInterfaceIdiom: controller?.traitCollection.userInterfaceIdiom ?? .phone, reflowableContentInsets: configuration.reflowableContentInsets ) } /// 根据当前配置生成阅读偏好设置。 func currentPreferences() -> RDEPUBPreferences { configuration.makePreferences() } /// 获取当前文本排版的单页尺寸,优先从 readerView 解析,兜底用布局上下文。 func currentTextPageSize() -> CGSize { 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 } } return currentLayoutContext().viewportSize } /// 根据当前配置生成文本渲染样式(字体、行距、颜色)。 func currentTextRenderStyle() -> RDEPUBTextRenderStyle { let font = configuration.fontChoice.font(ofSize: configuration.fontSize) let lineSpacing = max(font.lineHeight * (configuration.lineHeightMultiple - 1), 4) return RDEPUBTextRenderStyle( font: font, lineSpacing: lineSpacing, textColor: configuration.theme.contentTextColor, backgroundColor: configuration.theme.contentBackgroundColor ) } /// 根据页面尺寸和配置生成文本排版参数。 func currentTextLayoutConfig(pageSize: CGSize) -> RDEPUBTextLayoutConfig { return RDEPUBTextLayoutConfig( frameWidth: max(pageSize.width, 1), frameHeight: max(pageSize.height, 1), edgeInsets: configuration.reflowableContentInsets, numberOfColumns: configuration.numberOfColumns, columnGap: configuration.columnGap, // 小说正文更看重尽量铺满页面,避免页尾出现明显留白。 avoidOrphans: false, avoidWidows: false, avoidPageBreakInsideEnabled: true, hyphenation: true, imageMaxHeightRatio: 0.85, fallbackViewportSize: dependencies.environment.fallbackViewportSize ) } /// 获取当前配置对应的文本渲染器实例。 func resolvedTextRenderer() -> RDEPUBTextRenderer { dependencies.makeTextRenderer(configuration.textRenderingEngine) } /// 当前活跃的页面列表。 var activePages: [EPUBPage] { readingSession?.activePages ?? [] } /// 当前活跃的章节信息列表。 var activeChapters: [EPUBChapterInfo] { readingSession?.activeChapters ?? [] } /// 屏幕亮度代理属性,读写均转发给系统环境。 var currentBrightness: CGFloat { get { dependencies.environment.currentBrightness } set { dependencies.environment.currentBrightness = newValue } } /// 用新快照替换当前活跃的分页快照。 func replaceActiveSnapshot(_ snapshot: RDEPUBReadingSession.PaginationSnapshot) { readingSession?.setActiveSnapshot(snapshot) } /// 清除当前活跃快照,重置运行时状态。 func clearActiveSnapshot() { readingSession?.resetRuntimeState() } /// 工厂方法:创建 EPUB 解析器。 func makeParser() -> RDEPUBParser { dependencies.makeParser() } /// 工厂方法:创建 Web 内容分页计算器。 func makePaginator() -> RDEPUBPaginator { dependencies.makePaginator() } /// 工厂方法:创建 EPUB 文本图书构建器。 func makeTextBookBuilder(layoutConfig: RDEPUBTextLayoutConfig) -> RDEPUBTextBookBuilder { dependencies.makeTextBookBuilder(resolvedTextRenderer(), textBookCache, layoutConfig) } func makeChapterSummaryDiskCache() -> RDEPUBChapterSummaryDiskCache { let cachesDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first ?? FileManager.default.temporaryDirectory let bookID = (currentBookIdentifier ?? "default").sha256Hex let directory = cachesDirectory .appendingPathComponent("RDEPUBChapterSummaryCache", isDirectory: true) .appendingPathComponent(bookID, isDirectory: true) return RDEPUBChapterSummaryDiskCache(cacheDirectory: directory) } 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() ) } /// 使用预计算的 contentHash 构建缓存键,避免重复读取 HTML 和计算 SHA-256。 /// 后台批量解析必须走此版本。 func chapterCacheKey(forSpineIndex spineIndex: Int, precomputedContentHash: String) -> RDEPUBChapterCacheKey { chapterCacheKey( forSpineIndex: spineIndex, precomputedContentHash: precomputedContentHash, renderSignature: currentRenderSignature() ) } /// 使用固定的渲染签名与预计算 contentHash 构建缓存键。 /// 适合后台任务在启动时冻结分页参数后复用,避免 live context 漂移。 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 { dependencies.makePlainTextBookBuilder(resolvedTextRenderer(), 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 } /// 根据规范化 href 获取文本章节数据。 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() } }