- Add RDEPUBReaderUIState struct for centralized UI state model - Refactor RDEPUBReaderChromeCoordinator with makeUIState() and applyUIState() - Remove direct UI updates from RDEPUBReaderAnnotationCoordinator - Consolidate updateBookmarkChrome() into updateReaderChrome() - Update RDEPUBReaderContext, Runtime, and LocationCoordinator - Add reader problem fix development checklist documentation Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
239 lines
9.8 KiB
Swift
239 lines
9.8 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.onSearch = { [weak self] in
|
||
self?.toggleSearchBar()
|
||
}
|
||
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 }
|
||
let uiState = makeUIState()
|
||
applyUIState(uiState)
|
||
updateSearchBar()
|
||
}
|
||
|
||
/// 构建当前 UI 状态快照
|
||
func makeUIState() -> RDEPUBReaderUIState {
|
||
guard let controller else { return .empty }
|
||
return RDEPUBReaderUIState(
|
||
canToggleBookmark: controller.currentBookIdentifier != nil,
|
||
hasBookmarkAtCurrentLocation: hasBookmarkAtCurrentLocation(),
|
||
canShowBookmarks: !controller.activeBookmarks.isEmpty,
|
||
canAddHighlight: controller.configuration.allowsHighlights && controller.currentSelection != nil,
|
||
canShowHighlights: controller.configuration.allowsHighlights && !controller.activeHighlights.isEmpty,
|
||
showsTableOfContents: controller.configuration.showsTableOfContents,
|
||
allowsHighlights: controller.configuration.allowsHighlights,
|
||
showsSettingsPanel: controller.configuration.showsSettingsPanel
|
||
)
|
||
}
|
||
|
||
/// 将 UI 状态应用到顶部和底部工具栏
|
||
func applyUIState(_ state: RDEPUBReaderUIState) {
|
||
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(state.canToggleBookmark)
|
||
controller.topToolView.setBookmarkSelected(state.hasBookmarkAtCurrentLocation)
|
||
controller.bottomToolView.apply(theme: controller.configuration.theme)
|
||
controller.bottomToolView.updateVisibility(
|
||
showsTableOfContents: state.showsTableOfContents,
|
||
allowsHighlights: state.allowsHighlights,
|
||
showsSettingsPanel: state.showsSettingsPanel
|
||
)
|
||
controller.bottomToolView.setBookmarksEnabled(state.canShowBookmarks)
|
||
controller.bottomToolView.setAddHighlightEnabled(state.canAddHighlight)
|
||
controller.bottomToolView.setHighlightsEnabled(state.canShowHighlights)
|
||
}
|
||
|
||
/// 判断当前位置是否有书签
|
||
private func hasBookmarkAtCurrentLocation() -> Bool {
|
||
guard let controller else { return false }
|
||
return context.runtime?.annotationCoordinator.currentBookmark() != nil
|
||
}
|
||
|
||
/// 弹出阅读设置面板(字号、字体、行距、分栏、主题、亮度等)。
|
||
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.flattenedTableOfContentsItems(
|
||
from: controller.publication?.tableOfContents ?? [],
|
||
includePageNumbers: false
|
||
)
|
||
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)
|
||
}
|
||
|
||
/// 切换搜索栏的显示/隐藏状态。
|
||
func toggleSearchBar() {
|
||
guard let controller else { return }
|
||
if controller.isSearchBarVisible {
|
||
controller.hideSearchBar()
|
||
} else {
|
||
controller.showSearchBar()
|
||
}
|
||
}
|
||
|
||
/// 同步搜索栏的主题和匹配计数。
|
||
func updateSearchBar() {
|
||
guard let controller else { return }
|
||
controller.searchBarView.apply(theme: controller.configuration.theme)
|
||
if let searchState = controller.searchState {
|
||
if let index = searchState.currentMatchIndex {
|
||
controller.searchBarView.updateMatchCount(current: index + 1, total: searchState.matches.count)
|
||
} else if searchState.matches.isEmpty {
|
||
controller.searchBarView.showNoResults()
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 处理返回按钮点击,自动判断 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
|
||
}
|
||
}
|