feat: EPUB阅读器搜索、注释、CFI模块及大书远距跳转优化
- 实现EPUB阅读器搜索功能及选中注释功能 - 优化CFI模块,修复代码审查发现的11个问题 - 实现大书远距目录跳转与后台补全优化方案 - 优化设置面板与章节运行时联动 - 重构及大量改进优化
This commit is contained in:
@@ -1,32 +1,9 @@
|
||||
import UIKit
|
||||
|
||||
// MARK: - 文件说明
|
||||
|
||||
/// EPUB 阅读器主控制器
|
||||
/// EPUBUI 层的核心入口,提供开箱即用的完整阅读器体验
|
||||
/// 调用方只需传入 EPUB 文件 URL 或已构建的 TextBook,即可呈现完整的阅读界面
|
||||
///
|
||||
/// **架构位置:** EPUBUI(最顶层)→ Core → Parser → Foundation
|
||||
///
|
||||
/// **核心职责:**
|
||||
/// - EPUB 解析与分页(支持流式排版和固定布局)
|
||||
/// - 阅读位置的持久化与恢复
|
||||
/// - 高亮、书签的增删改查
|
||||
/// - 工具栏、目录、设置等 UI 面板的协调管理
|
||||
/// - 搜索功能(全文搜索、前后跳转)
|
||||
///
|
||||
/// **打开流程:** init(epubURL:) → viewDidLoad → RDEPUBParser.parse → paginatePublication → readerView.reloadData → restoreLocation
|
||||
|
||||
// MARK: - 阅读器主控制器
|
||||
|
||||
public final class RDEPUBReaderController: UIViewController {
|
||||
|
||||
// MARK: - 公开属性
|
||||
|
||||
/// 阅读器委托,用于接收阅读状态变更通知
|
||||
public weak var delegate: RDEPUBReaderDelegate?
|
||||
|
||||
/// 阅读器配置,修改后自动判断是否需要重新分页或刷新内容
|
||||
public var configuration: RDEPUBReaderConfiguration {
|
||||
didSet {
|
||||
readerContext.configuration = configuration
|
||||
@@ -54,65 +31,58 @@ public final class RDEPUBReaderController: UIViewController {
|
||||
}
|
||||
}
|
||||
|
||||
/// 当前阅读位置
|
||||
public var currentLocation: RDEPUBLocation? {
|
||||
currentVisibleLocation()
|
||||
}
|
||||
|
||||
/// 当前页码(从 1 开始),nil 表示尚未加载
|
||||
public var currentPageNumber: Int? {
|
||||
guard readerView.currentPage >= 0 else { return nil }
|
||||
return readerView.currentPage + 1
|
||||
}
|
||||
|
||||
/// 当前用户选中的文本(只读),用于创建高亮/批注
|
||||
public internal(set) var currentSelection: RDEPUBSelection? {
|
||||
get { readerContext.currentSelection }
|
||||
set { readerContext.currentSelection = newValue }
|
||||
}
|
||||
|
||||
/// 所有高亮标注
|
||||
public var highlights: [RDEPUBHighlight] {
|
||||
activeHighlights
|
||||
}
|
||||
|
||||
/// 所有书签
|
||||
public var bookmarks: [RDEPUBBookmark] {
|
||||
activeBookmarks
|
||||
}
|
||||
|
||||
/// 统一标注语义视图,对标 WXRead 的 WRBookmark 主模型。
|
||||
public var annotations: [RDEPUBAnnotation] {
|
||||
let merged = activeHighlights.map(\.annotation) + activeBookmarks.map(\.annotation)
|
||||
return merged.sorted { $0.createdAt < $1.createdAt }
|
||||
}
|
||||
|
||||
/// 原始目录树结构
|
||||
public var tableOfContents: [EPUBTableOfContentsItem] {
|
||||
publication?.tableOfContents ?? []
|
||||
}
|
||||
|
||||
/// 展平后的目录列表(用于目录面板展示,每个条目附带页码)
|
||||
public var flattenedTableOfContents: [RDEPUBReaderTableOfContentsItem] {
|
||||
guard let publication else { return [] }
|
||||
return flattenedTableOfContentsItems(from: publication.tableOfContents)
|
||||
}
|
||||
|
||||
/// 当前阅读位置所在的目录项
|
||||
public var currentTableOfContentsItem: RDEPUBReaderTableOfContentsItem? {
|
||||
resolvedCurrentTableOfContentsItem()
|
||||
}
|
||||
|
||||
// MARK: - 私有属性
|
||||
|
||||
/// EPUB 文件 URL
|
||||
let epubURL: URL
|
||||
|
||||
let persistence: RDEPUBReaderPersistence?
|
||||
|
||||
let dependencies: RDEPUBReaderDependencies
|
||||
|
||||
let readerView = RDReaderView()
|
||||
|
||||
let loadingIndicator: UIActivityIndicatorView = {
|
||||
UIActivityIndicatorView(style: .large)
|
||||
}()
|
||||
|
||||
let errorLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.numberOfLines = 0
|
||||
@@ -121,97 +91,115 @@ public final class RDEPUBReaderController: UIViewController {
|
||||
label.isHidden = true
|
||||
return label
|
||||
}()
|
||||
|
||||
let paginationHostView = UIView()
|
||||
|
||||
lazy var readerContext = RDEPUBReaderContext(controller: self)
|
||||
|
||||
var parser: RDEPUBParser? {
|
||||
get { readerContext.parser }
|
||||
set { readerContext.parser = newValue }
|
||||
}
|
||||
|
||||
var publication: RDEPUBPublication? {
|
||||
get { readerContext.publication }
|
||||
set { readerContext.publication = newValue }
|
||||
}
|
||||
|
||||
var readingSession: RDEPUBReadingSession? {
|
||||
get { readerContext.readingSession }
|
||||
set { readerContext.readingSession = newValue }
|
||||
}
|
||||
|
||||
var textBook: RDEPUBTextBook? {
|
||||
get { readerContext.textBook }
|
||||
set { readerContext.textBook = newValue }
|
||||
}
|
||||
|
||||
var activePages: [EPUBPage] {
|
||||
readerContext.activePages
|
||||
}
|
||||
|
||||
var activeChapters: [EPUBChapterInfo] {
|
||||
readerContext.activeChapters
|
||||
}
|
||||
|
||||
var activeBookmarks: [RDEPUBBookmark] {
|
||||
get { readerContext.activeBookmarks }
|
||||
set { readerContext.activeBookmarks = newValue }
|
||||
}
|
||||
|
||||
var activeHighlights: [RDEPUBHighlight] {
|
||||
get { readerContext.activeHighlights }
|
||||
set { readerContext.activeHighlights = newValue }
|
||||
}
|
||||
|
||||
lazy var topToolView = runtime.makeTopToolView()
|
||||
|
||||
lazy var bottomToolView = runtime.makeBottomToolView()
|
||||
|
||||
lazy var searchBarView = RDEPUBReaderSearchBarView()
|
||||
/// 搜索栏是否当前可见
|
||||
|
||||
private(set) var isSearchBarVisible = false
|
||||
|
||||
var currentBookIdentifier: String? {
|
||||
get { readerContext.currentBookIdentifier }
|
||||
set { readerContext.currentBookIdentifier = newValue }
|
||||
}
|
||||
|
||||
var textBookCache: RDEPUBTextBookCache { readerContext.textBookCache }
|
||||
|
||||
var currentBrightness: CGFloat {
|
||||
get { readerContext.currentBrightness }
|
||||
set { readerContext.currentBrightness = newValue }
|
||||
}
|
||||
|
||||
var didStartInitialLoad: Bool {
|
||||
get { readerContext.didStartInitialLoad }
|
||||
set { readerContext.didStartInitialLoad = newValue }
|
||||
}
|
||||
|
||||
var isRepaginating: Bool {
|
||||
get { readerContext.isRepaginating }
|
||||
set { readerContext.isRepaginating = newValue }
|
||||
}
|
||||
|
||||
var lastTextPaginationPageSize: CGSize? {
|
||||
get { readerContext.lastTextPaginationPageSize }
|
||||
set { readerContext.lastTextPaginationPageSize = newValue }
|
||||
}
|
||||
|
||||
var isReconcilingTextPaginationSize = false
|
||||
|
||||
var paginationToken: UUID {
|
||||
get { readerContext.paginationToken }
|
||||
set { readerContext.paginationToken = newValue }
|
||||
}
|
||||
|
||||
var paginator: RDEPUBPaginator? {
|
||||
get { readerContext.paginator }
|
||||
set { readerContext.paginator = newValue }
|
||||
}
|
||||
|
||||
var searchState: RDEPUBSearchState? {
|
||||
get { readerContext.searchState }
|
||||
set { readerContext.searchState = newValue }
|
||||
}
|
||||
|
||||
var isExternalTextBook: Bool {
|
||||
get { readerContext.isExternalTextBook }
|
||||
set { readerContext.isExternalTextBook = newValue }
|
||||
}
|
||||
|
||||
var textFileURL: URL? {
|
||||
get { readerContext.textFileURL }
|
||||
set { readerContext.textFileURL = newValue }
|
||||
}
|
||||
|
||||
lazy var readerAssemblyCoordinator = RDEPUBReaderAssemblyCoordinator(context: readerContext)
|
||||
|
||||
lazy var runtime = RDEPUBReaderRuntime(context: readerContext)
|
||||
|
||||
// MARK: - 初始化
|
||||
|
||||
/// 使用 EPUB 文件 URL 初始化阅读器
|
||||
/// 会自动从持久化存储中恢复用户的阅读偏好设置
|
||||
/// - Parameters:
|
||||
/// - epubURL: EPUB 文件的本地 URL
|
||||
/// - configuration: 阅读器配置,默认使用 .default
|
||||
/// - persistence: 持久化策略,默认使用 UserDefaults
|
||||
public init(
|
||||
epubURL: URL,
|
||||
configuration: RDEPUBReaderConfiguration = .default,
|
||||
@@ -225,7 +213,7 @@ public final class RDEPUBReaderController: UIViewController {
|
||||
self.dependencies = dependencies
|
||||
let brightness = max(0, min(1, persistedSettings?.brightness ?? dependencies.environment.currentBrightness))
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
// Sync context after super.init
|
||||
|
||||
readerContext.dependencies = dependencies
|
||||
readerContext.configuration = self.configuration
|
||||
readerContext.epubURL = epubURL
|
||||
@@ -245,15 +233,6 @@ public final class RDEPUBReaderController: UIViewController {
|
||||
)
|
||||
}
|
||||
|
||||
/// 使用已构建的 TextBook 初始化,跳过 EPUB 解析流程
|
||||
/// 适用于 TXT 等纯文本文件,调用方需自行构建 TextBook
|
||||
/// - Parameters:
|
||||
/// - textBook: 已分页的 TextBook 实例
|
||||
/// - bookIdentifier: 书籍唯一标识符,用于持久化
|
||||
/// - title: 书籍标题
|
||||
/// - textFileURL: 原始文本文件 URL,用于重建 TextBook
|
||||
/// - configuration: 阅读器配置
|
||||
/// - persistence: 持久化策略
|
||||
public convenience init(
|
||||
textBook: RDEPUBTextBook,
|
||||
bookIdentifier: String,
|
||||
@@ -317,10 +296,6 @@ public final class RDEPUBReaderController: UIViewController {
|
||||
runtime.viewportMonitor.viewWillTransition(with: coordinator)
|
||||
}
|
||||
|
||||
// MARK: - 搜索栏管理
|
||||
|
||||
/// 显示搜索栏。工具栏可见时立即添加到视图层级并滑入动画;否则仅标记状态,
|
||||
/// 等待 `handleToolViewVisibilityChanged` 在工具栏显示时再安装。
|
||||
func showSearchBar() {
|
||||
guard !isSearchBarVisible else { return }
|
||||
isSearchBarVisible = true
|
||||
@@ -367,7 +342,6 @@ public final class RDEPUBReaderController: UIViewController {
|
||||
}
|
||||
}
|
||||
|
||||
/// 将搜索栏添加到 readerView 并执行滑入动画
|
||||
private func installSearchBarView() {
|
||||
guard searchBarView.superview == nil else { return }
|
||||
searchBarView.apply(theme: configuration.theme)
|
||||
@@ -403,8 +377,6 @@ public final class RDEPUBReaderController: UIViewController {
|
||||
}
|
||||
}
|
||||
|
||||
/// 隐藏搜索栏,带动画滑出
|
||||
/// - Parameter clearSearch: 是否同时清除搜索状态
|
||||
func hideSearchBar(clearSearch: Bool = false) {
|
||||
guard isSearchBarVisible else { return }
|
||||
isSearchBarVisible = false
|
||||
@@ -425,7 +397,6 @@ public final class RDEPUBReaderController: UIViewController {
|
||||
}
|
||||
}
|
||||
|
||||
/// 同步搜索栏匹配计数
|
||||
func updateSearchCount() {
|
||||
guard let searchState else {
|
||||
searchBarView.showNoResults()
|
||||
@@ -438,7 +409,6 @@ public final class RDEPUBReaderController: UIViewController {
|
||||
)
|
||||
}
|
||||
|
||||
/// 当工具栏可见性变化时同步搜索栏(由 RDReaderView 回调调用)
|
||||
func handleToolViewVisibilityChanged(isVisible: Bool) {
|
||||
if isVisible {
|
||||
if isSearchBarVisible {
|
||||
@@ -456,6 +426,7 @@ public final class RDEPUBReaderController: UIViewController {
|
||||
}
|
||||
|
||||
private extension RDEPUBReaderController {
|
||||
|
||||
func searchResultSections(for searchState: RDEPUBSearchState) -> [RDEPUBReaderSearchSection] {
|
||||
let groupedMatches = Dictionary(grouping: Array(searchState.matches.enumerated()), by: { entry in
|
||||
searchSectionTitle(for: entry.element)
|
||||
|
||||
Reference in New Issue
Block a user