Phase 1: Context 拆分 - 新增 RDEPUBReaderState/RDEPUBReaderEnvironment/RDEPUBReaderServices - RDEPUBReaderContext 改为过渡门面,代理到 State/Environment/Services Phase 2: Runtime 拆分 - 新增 RDEPUBPresentationRuntime 处理分页状态管理 - 新增 RDEPUBChapterWarmupOrchestrator 处理章节预热与加载编排 - RDEPUBReaderRuntime 从 1277 行收缩,公共 API 转发到新 facade Phase 0.5: 性能优化 - prepareOnDemandChapter 支持异步模式(allowSynchronousLoad: false) - extendPartialBookPageMapIfNeeded 改为 DispatchGroup 并发加载 - RDEPUBChapterOffsetMap.cfiMap 加 NSLock 保护数据竞争 - CFI Map 构建延迟到后台队列(scheduleDeferredCFIMapBuildIfNeeded) - RDEPUBTextPageRenderView 引入静态位图缓存 - RDEPUBTextContentView 新增 loadingSpinner 占位页 Phase 3: 状态机 - 新增 RDEPUBNavigationStateMachine(含 DEBUG 合法转换校验) - 新增 RDEPUBPaginationState 记录分页来源 Review 修复 - makeSummary 重复方法合并 - ensureNavigationTargetAvailable 同步路径加注释标记 UI 测试 - 新增 AsyncChapterLoadingTests(20 个测试,覆盖全部架构整改场景) - 跨章节翻页、延迟 CFI、状态机、内存警告、预加载、位置恢复等
392 lines
12 KiB
Swift
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 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()
|
|
}
|
|
}
|