ReadViewSDK/Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBReaderContext.swift
shenlei d15f20b097 feat: 交互协调器拆分、附件提示、暗色图片适配、选区放大镜及文档清理
- 拆分 ContentDelegates/TextContentView 为独立协调器(InteractionCoordinator、LocationResolution、ExternalLinks、AttachmentTooltip)
- 新增 RDEPUBAttachmentTooltipView/OverlayView 附件气泡提示
- 新增 RDEPUBDarkImageAdjuster 暗色模式图片亮度适配
- 新增 RDEPUBSelectionLoupeView 选区放大镜
- 新增 MetadataParseWorker/CancellationController 元数据解析取消机制
- 重构 PresentationRuntime/PaginationCoordinator 精简职责
- 优化 ChapterLoader/WarmupOrchestrator 异步章节加载
- CFI 模块微调与 NoteModels 更新
- 清理冗余文档,更新架构/UML/业务逻辑文档

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-24 17:47:24 +08:00

392 lines
12 KiB
Swift

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 pendingPageMapUpdates: [RDEPUBPendingPageMapUpdate] {
get { state.pendingPageMapUpdates }
set { state.pendingPageMapUpdates = 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)?.rd_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()
}
}