feat: unify reader chrome state management
- 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>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
d15187b730
commit
ed390da147
@@ -31,7 +31,7 @@ extension RDEPUBReaderController: RDReaderDataSource, RDReaderDelegate {
|
||||
totalPages: pageCountOfReaderView(readerView: readerView),
|
||||
configuration: configuration,
|
||||
highlights: textHighlights(for: resolvedPage.page),
|
||||
searchState: searchState
|
||||
searchState: searchState(for: resolvedPage.page)
|
||||
)
|
||||
return contentView
|
||||
}
|
||||
@@ -46,7 +46,7 @@ extension RDEPUBReaderController: RDReaderDataSource, RDReaderDelegate {
|
||||
totalPages: textBook.pages.count,
|
||||
configuration: configuration,
|
||||
highlights: textHighlights(for: page),
|
||||
searchState: searchState
|
||||
searchState: searchState(for: page)
|
||||
)
|
||||
return contentView
|
||||
}
|
||||
@@ -91,6 +91,57 @@ extension RDEPUBReaderController: RDReaderDataSource, RDReaderDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
/// 返回当前文本页需要展示的搜索状态。
|
||||
/// 这里会先按页过滤命中结果,并把 currentMatchIndex 重映射到当前页内的局部索引,
|
||||
/// 这样渲染层不需要再关心 href 规范化差异。
|
||||
private func searchState(for page: RDEPUBTextPage) -> RDEPUBSearchState? {
|
||||
guard let globalSearchState = searchState else { return nil }
|
||||
|
||||
let matches = globalSearchState.matches.filter { searchMatch in
|
||||
searchMatchBelongsToPage(searchMatch, page: page)
|
||||
}
|
||||
guard !matches.isEmpty || globalSearchState.currentMatch != nil else {
|
||||
return globalSearchState.matches.isEmpty ? globalSearchState : nil
|
||||
}
|
||||
|
||||
let currentMatchIndex = globalSearchState.currentMatch.flatMap { currentMatch in
|
||||
matches.firstIndex(of: currentMatch)
|
||||
}
|
||||
return RDEPUBSearchState(
|
||||
keyword: globalSearchState.keyword,
|
||||
matches: matches,
|
||||
currentMatchIndex: currentMatchIndex
|
||||
)
|
||||
}
|
||||
|
||||
private func searchMatchBelongsToPage(_ searchMatch: RDEPUBSearchMatch, page: RDEPUBTextPage) -> Bool {
|
||||
if let textBook,
|
||||
let chapterData = textBook.chapterData(for: page.href),
|
||||
let range = chapterData.absoluteRange(for: searchMatch) {
|
||||
return NSIntersectionRange(range, page.contentRange).length > 0
|
||||
}
|
||||
|
||||
let pageHref = normalizedPageHref(for: page)
|
||||
let matchHref = normalizedSearchHref(searchMatch.href)
|
||||
guard pageHref == matchHref else { return false }
|
||||
|
||||
guard let rangeLocation = searchMatch.rangeLocation else {
|
||||
return false
|
||||
}
|
||||
let range = NSRange(location: rangeLocation, length: searchMatch.rangeLength)
|
||||
return NSIntersectionRange(range, page.contentRange).length > 0
|
||||
}
|
||||
|
||||
private func normalizedPageHref(for page: RDEPUBTextPage) -> String {
|
||||
guard let publication else { return page.href }
|
||||
return publication.resourceResolver.normalizedHref(page.href) ?? page.href
|
||||
}
|
||||
|
||||
private func normalizedSearchHref(_ href: String) -> String {
|
||||
guard let publication else { return href }
|
||||
return publication.resourceResolver.normalizedHref(href) ?? href
|
||||
}
|
||||
|
||||
/// 根据规范化 href 获取文本章节数据
|
||||
func textChapterData(forNormalizedHref href: String) -> RDEPUBChapterData? {
|
||||
readerContext.textChapterData(forNormalizedHref: href)
|
||||
|
||||
@@ -138,11 +138,6 @@ extension RDEPUBReaderController {
|
||||
runtime.updateReaderChrome()
|
||||
}
|
||||
|
||||
/// 更新书签按钮状态
|
||||
func updateBookmarkChrome() {
|
||||
runtime.updateBookmarkChrome()
|
||||
}
|
||||
|
||||
/// 展示书签管理界面
|
||||
func presentBookmarksManager() {
|
||||
runtime.presentBookmarksManager()
|
||||
|
||||
+3
-12
@@ -38,9 +38,7 @@ final class RDEPUBReaderAnnotationCoordinator {
|
||||
controller.readerView.isShowToolView == false {
|
||||
controller.readerView.tapCenter()
|
||||
}
|
||||
controller.bottomToolView.setAddHighlightEnabled(
|
||||
controller.configuration.allowsHighlights && controller.currentSelection != nil
|
||||
)
|
||||
controller.updateReaderChrome()
|
||||
controller.delegate?.epubReader(controller, didChangeSelection: controller.currentSelection)
|
||||
}
|
||||
|
||||
@@ -321,13 +319,6 @@ final class RDEPUBReaderAnnotationCoordinator {
|
||||
return controller.restoreReadingLocation(bookmark.location, animated: animated)
|
||||
}
|
||||
|
||||
/// 同步顶部书签按钮和底部书签列表按钮的 UI 状态。
|
||||
func updateBookmarkChrome() {
|
||||
guard let controller else { return }
|
||||
controller.topToolView.setBookmarkSelected(currentBookmark() != nil)
|
||||
controller.bottomToolView.setBookmarksEnabled(!controller.activeBookmarks.isEmpty)
|
||||
}
|
||||
|
||||
/// 弹出书签管理器,支持查看、跳转和删除书签。
|
||||
func presentBookmarksManager() {
|
||||
guard let controller else { return }
|
||||
@@ -468,10 +459,10 @@ final class RDEPUBReaderAnnotationCoordinator {
|
||||
guard let currentBookIdentifier = controller.currentBookIdentifier else { return }
|
||||
controller.persistence?.saveBookmarks(controller.activeBookmarks, for: currentBookIdentifier)
|
||||
controller.delegate?.epubReader(controller, didUpdateBookmarks: controller.activeBookmarks)
|
||||
updateBookmarkChrome()
|
||||
controller.updateReaderChrome()
|
||||
}
|
||||
|
||||
private func currentBookmark() -> RDEPUBBookmark? {
|
||||
func currentBookmark() -> RDEPUBBookmark? {
|
||||
guard let controller else { return nil }
|
||||
guard let location = scopedBookmarkLocation(controller.currentVisibleLocation()) else {
|
||||
return nil
|
||||
|
||||
@@ -57,6 +57,29 @@ final class RDEPUBReaderChromeCoordinator {
|
||||
|
||||
/// 同步更新顶部和底部工具栏的主题、标题、按钮可用性等状态。
|
||||
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(
|
||||
@@ -64,22 +87,23 @@ final class RDEPUBReaderChromeCoordinator {
|
||||
?? controller.parser?.metadata.title
|
||||
?? controller.epubURL.deletingPathExtension().lastPathComponent
|
||||
)
|
||||
controller.topToolView.setBookmarkEnabled(controller.currentBookIdentifier != nil)
|
||||
controller.topToolView.setBookmarkEnabled(state.canToggleBookmark)
|
||||
controller.topToolView.setBookmarkSelected(state.hasBookmarkAtCurrentLocation)
|
||||
controller.bottomToolView.apply(theme: controller.configuration.theme)
|
||||
controller.bottomToolView.updateVisibility(
|
||||
showsTableOfContents: controller.configuration.showsTableOfContents,
|
||||
allowsHighlights: controller.configuration.allowsHighlights,
|
||||
showsSettingsPanel: controller.configuration.showsSettingsPanel
|
||||
showsTableOfContents: state.showsTableOfContents,
|
||||
allowsHighlights: state.allowsHighlights,
|
||||
showsSettingsPanel: state.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()
|
||||
updateSearchBar()
|
||||
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
|
||||
}
|
||||
|
||||
/// 弹出阅读设置面板(字号、字体、行距、分栏、主题、亮度等)。
|
||||
|
||||
@@ -369,9 +369,4 @@ final class RDEPUBReaderContext {
|
||||
func applyReaderViewConfiguration() {
|
||||
controller?.applyReaderViewConfiguration()
|
||||
}
|
||||
|
||||
/// 同步书签按钮的 UI 状态。
|
||||
func updateBookmarkChrome() {
|
||||
controller?.updateBookmarkChrome()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,6 +87,6 @@ final class RDEPUBReaderLocationCoordinator {
|
||||
controller.persistence?.saveLocation(location, for: currentBookIdentifier)
|
||||
controller.delegate?.epubReader(controller, didUpdateLocation: location)
|
||||
controller.delegate?.epubReader(controller, didUpdateCurrentTableOfContentsItem: controller.currentTableOfContentsItem)
|
||||
controller.updateBookmarkChrome()
|
||||
controller.updateReaderChrome()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,10 +193,6 @@ final class RDEPUBReaderRuntime {
|
||||
annotationCoordinator.go(toBookmarkID: id, animated: animated)
|
||||
}
|
||||
|
||||
func updateBookmarkChrome() {
|
||||
annotationCoordinator.updateBookmarkChrome()
|
||||
}
|
||||
|
||||
func presentBookmarksManager() {
|
||||
annotationCoordinator.presentBookmarksManager()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import Foundation
|
||||
|
||||
/// 阅读器 UI 状态模型:统一管理顶部/底部工具栏按钮的可用性和显示状态。
|
||||
///
|
||||
/// 解决问题:之前书签、高亮等按钮的状态分散在 ChromeCoordinator 和 AnnotationCoordinator 中,
|
||||
/// 导致状态更新入口不一致,容易出现不同步的情况。
|
||||
struct RDEPUBReaderUIState {
|
||||
/// 顶部书签按钮是否可用(当前有书籍标识时可用)
|
||||
let canToggleBookmark: Bool
|
||||
/// 顶部书签按钮是否选中(当前位置已加书签时选中)
|
||||
let hasBookmarkAtCurrentLocation: Bool
|
||||
/// 底部书签列表按钮是否可用(有书签数据时可用)
|
||||
let canShowBookmarks: Bool
|
||||
/// 底部新建标注按钮是否可用(有选中文本时可用)
|
||||
let canAddHighlight: Bool
|
||||
/// 底部高亮列表按钮是否可用(有高亮数据时可用)
|
||||
let canShowHighlights: Bool
|
||||
/// 是否显示目录按钮
|
||||
let showsTableOfContents: Bool
|
||||
/// 是否显示高亮相关按钮(新建标注和高亮列表)
|
||||
let allowsHighlights: Bool
|
||||
/// 是否显示设置按钮
|
||||
let showsSettingsPanel: Bool
|
||||
}
|
||||
|
||||
extension RDEPUBReaderUIState {
|
||||
/// 默认的空状态
|
||||
static let empty = RDEPUBReaderUIState(
|
||||
canToggleBookmark: false,
|
||||
hasBookmarkAtCurrentLocation: false,
|
||||
canShowBookmarks: false,
|
||||
canAddHighlight: false,
|
||||
canShowHighlights: false,
|
||||
showsTableOfContents: true,
|
||||
allowsHighlights: true,
|
||||
showsSettingsPanel: true
|
||||
)
|
||||
}
|
||||
@@ -64,7 +64,7 @@ final class RDEPUBTextAnnotationOverlay: RDEPUBSelectionOverlayView {
|
||||
let pageStart = pageRange.lowerBound
|
||||
let pageEndExclusive = pageRange.upperBound
|
||||
|
||||
for match in searchState.matches where match.href == page.href {
|
||||
for match in searchState.matches {
|
||||
guard let matchStart = match.rangeLocation else { continue }
|
||||
let matchEnd = matchStart + match.rangeLength
|
||||
let overlapStart = max(matchStart, pageStart)
|
||||
@@ -100,7 +100,7 @@ final class RDEPUBTextAnnotationOverlay: RDEPUBSelectionOverlayView {
|
||||
let normalColor = UIColor(red: 248 / 255, green: 225 / 255, blue: 108 / 255, alpha: 0.55)
|
||||
let activeColor = UIColor(red: 255 / 255, green: 159 / 255, blue: 67 / 255, alpha: 0.75)
|
||||
|
||||
for match in searchState.matches where match.href == page.href {
|
||||
for match in searchState.matches {
|
||||
guard let matchStart = match.rangeLocation else { continue }
|
||||
let matchEnd = matchStart + match.rangeLength
|
||||
let overlapStart = max(matchStart, pageStart)
|
||||
|
||||
@@ -40,6 +40,8 @@ final class RDEPUBTextContentView: UIView, UIGestureRecognizerDelegate {
|
||||
let view = RDEPUBTextPageRenderView()
|
||||
view.backgroundColor = .clear
|
||||
view.isOpaque = false
|
||||
view.isAccessibilityElement = true
|
||||
view.accessibilityTraits = .staticText
|
||||
return view
|
||||
}()
|
||||
|
||||
@@ -118,6 +120,7 @@ final class RDEPUBTextContentView: UIView, UIGestureRecognizerDelegate {
|
||||
} else {
|
||||
self.hideSelectionMenu()
|
||||
}
|
||||
self.updateAccessibilityDecorationSummary()
|
||||
self.delegate?.textContentView(self, didChangeSelection: selection)
|
||||
}
|
||||
selectionController.pageProvider = { [weak self] in self?.currentPage }
|
||||
@@ -250,6 +253,7 @@ final class RDEPUBTextContentView: UIView, UIGestureRecognizerDelegate {
|
||||
backgroundOverlayView.applyDecorations(bgDecorations)
|
||||
overlayView.applyDecorations(fgDecorations)
|
||||
#endif
|
||||
updateAccessibilityDecorationSummary()
|
||||
|
||||
delegate?.textContentView(self, didChangeSelection: nil)
|
||||
setNeedsLayout()
|
||||
@@ -262,6 +266,7 @@ final class RDEPUBTextContentView: UIView, UIGestureRecognizerDelegate {
|
||||
selectionController.clearSelection(renderView: coreTextRenderView)
|
||||
overlayView.clearSelection()
|
||||
backgroundOverlayView.clearSelection()
|
||||
updateAccessibilityDecorationSummary()
|
||||
UIMenuController.shared.setMenuVisible(false, animated: true)
|
||||
}
|
||||
|
||||
@@ -570,13 +575,21 @@ final class RDEPUBTextContentView: UIView, UIGestureRecognizerDelegate {
|
||||
#endif
|
||||
}
|
||||
|
||||
private func updateAccessibilityDecorationSummary() {
|
||||
#if canImport(DTCoreText)
|
||||
coreTextContentView.accessibilityValue = [
|
||||
backgroundOverlayView.decorationSummary(),
|
||||
overlayView.decorationSummary()
|
||||
].joined(separator: " | ")
|
||||
#endif
|
||||
}
|
||||
|
||||
private func highlight(at point: CGPoint) -> RDEPUBHighlight? {
|
||||
guard let page = currentPage else { return nil }
|
||||
let absoluteRange = backgroundOverlayView.absoluteRange(at: point) ?? overlayView.absoluteRange(at: point)
|
||||
guard let absoluteRange else { return nil }
|
||||
let matches = currentHighlights.filter { highlight in
|
||||
guard highlight.location.href == page.href,
|
||||
let range = RDEPUBTextOffsetRangeInfo.decode(from: highlight.rangeInfo)?.nsRange else {
|
||||
guard let range = RDEPUBTextOffsetRangeInfo.decode(from: highlight.rangeInfo)?.nsRange else {
|
||||
return false
|
||||
}
|
||||
return NSIntersectionRange(range, absoluteRange).length > 0
|
||||
|
||||
Reference in New Issue
Block a user