源码注释: - 为 ~60 个 Swift 文件补充缺失的 doc comment(file header、类型、属性、方法) - 修正 4 处错误注释:翻页模式数量、搜索行为描述、手势识别器描述、悬空文档块 文档维护: - 删除重复文档:WXRead/读书EPUB阅读器实现架构.md(与微信读书版完全一致) - 合并重叠文档:阅读器规划.md → 阅读器功能开发计划.md(单一真值) - 修正过时内容:所有文档中"四种翻页模式"→"三种",移除 horizontalCoverScroll - 更新架构图:补齐 EPUBUI/ReaderController、Paging/、Typesetter/ 等子目录 - 更新 index.md 索引:新增开发计划和架构对比文档引用
185 lines
7.6 KiB
Swift
185 lines
7.6 KiB
Swift
import UIKit
|
||
|
||
/// EPUB 阅读器 Chrome 协调器:负责顶部/底部工具栏的创建、更新和交互处理。
|
||
///
|
||
/// 职责:
|
||
/// - 创建并配置顶部工具栏(返回、书签按钮)
|
||
/// - 创建并配置底部工具栏(目录、书签、高亮、设置按钮)
|
||
/// - 同步工具栏的主题和状态
|
||
/// - 弹出设置面板和目录面板
|
||
/// - 处理返回按钮的关闭逻辑
|
||
final class RDEPUBReaderChromeCoordinator {
|
||
private unowned let context: RDEPUBReaderContext
|
||
|
||
init(context: RDEPUBReaderContext) {
|
||
self.context = context
|
||
}
|
||
|
||
private var controller: RDEPUBReaderController? {
|
||
context.controller
|
||
}
|
||
|
||
/// 创建顶部工具栏视图,绑定返回和书签切换回调。
|
||
func makeTopToolView() -> RDEPUBReaderTopToolView {
|
||
let toolView = RDEPUBReaderTopToolView()
|
||
toolView.onBack = { [weak self] in
|
||
self?.handleBackAction()
|
||
}
|
||
toolView.onToggleBookmark = { [weak self] in
|
||
_ = self?.context.runtime?.toggleBookmark()
|
||
}
|
||
return toolView
|
||
}
|
||
|
||
/// 创建底部工具栏视图,绑定目录、书签、高亮、设置等回调。
|
||
func makeBottomToolView() -> RDEPUBReaderBottomToolView {
|
||
let toolView = RDEPUBReaderBottomToolView()
|
||
toolView.onShowTableOfContents = { [weak self] in
|
||
self?.presentTableOfContents()
|
||
}
|
||
toolView.onShowBookmarks = { [weak self] in
|
||
self?.context.runtime?.presentBookmarksManager()
|
||
}
|
||
toolView.onShowHighlights = { [weak self] in
|
||
self?.context.runtime?.presentHighlightsManager()
|
||
}
|
||
toolView.onAddHighlight = { [weak self] in
|
||
self?.context.runtime?.presentAnnotationCreation()
|
||
}
|
||
toolView.onShowSettings = { [weak self] in
|
||
self?.presentSettings()
|
||
}
|
||
return toolView
|
||
}
|
||
|
||
/// 同步更新顶部和底部工具栏的主题、标题、按钮可用性等状态。
|
||
func updateReaderChrome() {
|
||
guard let controller else { return }
|
||
controller.topToolView.apply(theme: controller.configuration.theme)
|
||
controller.topToolView.setTitle(
|
||
controller.title
|
||
?? controller.parser?.metadata.title
|
||
?? controller.epubURL.deletingPathExtension().lastPathComponent
|
||
)
|
||
controller.topToolView.setBookmarkEnabled(controller.currentBookIdentifier != nil)
|
||
controller.bottomToolView.apply(theme: controller.configuration.theme)
|
||
controller.bottomToolView.updateVisibility(
|
||
showsTableOfContents: controller.configuration.showsTableOfContents,
|
||
allowsHighlights: controller.configuration.allowsHighlights,
|
||
showsSettingsPanel: controller.configuration.showsSettingsPanel
|
||
)
|
||
controller.bottomToolView.setBookmarksEnabled(!controller.activeBookmarks.isEmpty)
|
||
controller.bottomToolView.setAddHighlightEnabled(
|
||
controller.configuration.allowsHighlights && controller.currentSelection != nil
|
||
)
|
||
controller.bottomToolView.setHighlightsEnabled(
|
||
controller.configuration.allowsHighlights && !controller.activeHighlights.isEmpty
|
||
)
|
||
controller.updateBookmarkChrome()
|
||
}
|
||
|
||
/// 弹出阅读设置面板(字号、字体、行距、分栏、主题、亮度等)。
|
||
func presentSettings() {
|
||
guard let controller else { return }
|
||
guard controller.configuration.showsSettingsPanel else { return }
|
||
let settingsController = RDEPUBReaderSettingsViewController(
|
||
configuration: controller.configuration,
|
||
brightness: controller.currentBrightness
|
||
)
|
||
settingsController.onBrightnessChange = { [weak controller] brightness in
|
||
controller?.setScreenBrightness(brightness)
|
||
}
|
||
settingsController.onFontSizeChange = { [weak controller] fontSize in
|
||
controller?.updateConfiguration { $0.fontSize = fontSize }
|
||
}
|
||
settingsController.onFontChoiceChange = { [weak controller] fontChoice in
|
||
controller?.updateConfiguration { $0.fontChoice = fontChoice }
|
||
}
|
||
settingsController.onLineHeightChange = { [weak controller] lineHeightMultiple in
|
||
controller?.updateConfiguration { $0.lineHeightMultiple = lineHeightMultiple }
|
||
}
|
||
settingsController.onColumnCountChange = { [weak controller] numberOfColumns in
|
||
controller?.updateConfiguration { $0.numberOfColumns = numberOfColumns }
|
||
}
|
||
settingsController.onDisplayTypeChange = { [weak controller] displayType in
|
||
controller?.updateConfiguration { $0.displayType = displayType }
|
||
}
|
||
settingsController.onThemeChange = { [weak controller] theme in
|
||
controller?.updateConfiguration { $0.theme = theme }
|
||
}
|
||
|
||
let navigationController = UINavigationController(rootViewController: settingsController)
|
||
navigationController.modalPresentationStyle = .pageSheet
|
||
controller.present(navigationController, animated: true)
|
||
}
|
||
|
||
/// 弹出目录列表面板,支持点击跳转到指定章节。
|
||
func presentTableOfContents() {
|
||
guard let controller else { return }
|
||
guard controller.configuration.showsTableOfContents else { return }
|
||
let items = controller.flattenedTableOfContents
|
||
guard !items.isEmpty else { return }
|
||
|
||
let chapterController = RDEPUBReaderChapterListController(
|
||
items: items,
|
||
currentItem: controller.currentTableOfContentsItem,
|
||
theme: controller.configuration.theme
|
||
)
|
||
chapterController.onSelectItem = { [weak controller] item in
|
||
guard let controller else { return }
|
||
chapterController.dismiss(animated: true) {
|
||
_ = controller.go(toTableOfContentsItem: item, animated: true)
|
||
}
|
||
}
|
||
|
||
let navigationController = UINavigationController(rootViewController: chapterController)
|
||
navigationController.modalPresentationStyle = .pageSheet
|
||
controller.present(navigationController, animated: true)
|
||
}
|
||
|
||
/// 处理返回按钮点击,自动判断 pop 或 dismiss 方式关闭阅读器。
|
||
func handleBackAction() {
|
||
guard let controller else { return }
|
||
close(controller)
|
||
}
|
||
|
||
private func close(_ controller: UIViewController) {
|
||
let target = closestDismissTarget(from: controller)
|
||
if let navigationController = target.navigationController,
|
||
navigationController.viewControllers.first !== target {
|
||
navigationController.popViewController(animated: true)
|
||
return
|
||
}
|
||
|
||
if let navigationController = target.navigationController,
|
||
navigationController.presentingViewController != nil {
|
||
navigationController.dismiss(animated: true)
|
||
return
|
||
}
|
||
|
||
if target.presentingViewController != nil {
|
||
target.dismiss(animated: true)
|
||
return
|
||
}
|
||
|
||
controller.dismiss(animated: true)
|
||
}
|
||
|
||
private func closestDismissTarget(from controller: UIViewController) -> UIViewController {
|
||
var candidate: UIViewController = controller
|
||
var current = controller.parent
|
||
while let parent = current {
|
||
if let navigationController = parent.navigationController,
|
||
navigationController.viewControllers.contains(parent) {
|
||
return parent
|
||
}
|
||
if parent.presentingViewController != nil || parent.navigationController?.presentingViewController != nil {
|
||
return parent
|
||
}
|
||
candidate = parent
|
||
current = parent.parent
|
||
}
|
||
return candidate
|
||
}
|
||
}
|