ReadViewSDK/Sources/RDReaderView/EPUBUI/RDEPUBReaderHighlightsViewController.swift
shenlei c76beed03c feat(reader): 实现大书快速进入与增量分页路径
- PaginationCoordinator: 新增 textReflowable 快速进入路径,支持增量章节构建
  与 staged 合并策略,避免整书一次性构建的主线程阻塞
- TextBookBuilder: 重构为支持单章构建与增量合并模式
- ChapterPageCounter: 优化分页计数逻辑,支持章节级分页缓存
- PageFrameFactory: 新增 CoreText 页面帧工厂,提升排版性能
- ReaderContext/ReaderRuntime: 适配增量分页状态管理

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-02 21:16:57 +08:00

353 lines
14 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import UIKit
// MARK: -
/// EPUB
/// //
final class RDEPUBReaderHighlightsViewController: UITableViewController {
///
var onSelectHighlight: ((RDEPUBHighlight) -> Void)?
///
var onUpdateHighlight: ((RDEPUBHighlight) -> Void)?
///
var onDeleteHighlight: ((RDEPUBHighlight) -> Void)?
///
private var highlights: [RDEPUBHighlight]
///
private let theme: RDEPUBReaderTheme
///
private let sectionTitleProvider: (RDEPUBHighlight) -> String?
///
private let filterControl = UISegmentedControl(items: ["全部", "批注", "高亮"])
private var filteredHighlights: [RDEPUBHighlight] {
switch filterControl.selectedSegmentIndex {
case 1:
return highlights.filter(\.hasNote)
case 2:
return highlights.filter { $0.style == .highlight }
default:
return highlights
}
}
init(
highlights: [RDEPUBHighlight],
theme: RDEPUBReaderTheme,
sectionTitleProvider: @escaping (RDEPUBHighlight) -> String?
) {
self.highlights = highlights.sorted { $0.createdAt > $1.createdAt }
self.theme = theme
self.sectionTitleProvider = sectionTitleProvider
super.init(style: .plain)
title = "标注"
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
tableView.tableFooterView = UIView(frame: .zero)
tableView.separatorInset = UIEdgeInsets(top: 0, left: 16, bottom: 0, right: 16)
configureFilterControl()
applyTheme()
updateEmptyState()
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
filteredHighlights.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cellIdentifier = "HighlightCell"
let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier) ?? UITableViewCell(style: .subtitle, reuseIdentifier: cellIdentifier)
let highlight = filteredHighlights[indexPath.row]
cell.backgroundColor = theme.contentBackgroundColor
cell.textLabel?.textColor = theme.contentTextColor
cell.textLabel?.font = UIFont.systemFont(ofSize: 15, weight: .medium)
cell.textLabel?.numberOfLines = 2
cell.textLabel?.text = titleText(for: highlight)
cell.detailTextLabel?.textColor = theme.contentTextColor.withAlphaComponent(0.7)
cell.detailTextLabel?.font = UIFont.systemFont(ofSize: 12)
cell.detailTextLabel?.numberOfLines = 3
cell.detailTextLabel?.text = detailText(for: highlight)
cell.accessoryType = .disclosureIndicator
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
presentActions(for: filteredHighlights[indexPath.row], sourceIndexPath: indexPath)
}
private func configureFilterControl() {
filterControl.selectedSegmentIndex = 0
filterControl.addTarget(self, action: #selector(filterChangedAction), for: .valueChanged)
navigationItem.titleView = filterControl
}
private func applyTheme() {
tableView.backgroundColor = theme.contentBackgroundColor
navigationController?.navigationBar.tintColor = theme.toolControlTextColor
navigationController?.navigationBar.barTintColor = theme.toolBackgroundColor
navigationController?.navigationBar.titleTextAttributes = [.foregroundColor: theme.toolControlTextColor]
}
private func updateEmptyState() {
guard filteredHighlights.isEmpty else {
tableView.backgroundView = nil
return
}
let label = UILabel()
label.text = emptyStateText()
label.textAlignment = .center
label.textColor = theme.contentTextColor.withAlphaComponent(0.7)
label.numberOfLines = 0
tableView.backgroundView = label
}
@objc private func filterChangedAction() {
tableView.reloadData()
updateEmptyState()
}
private func emptyStateText() -> String {
switch filterControl.selectedSegmentIndex {
case 1:
return "暂无批注"
case 2:
return "暂无划线"
default:
return "暂无标注"
}
}
private func detailText(for highlight: RDEPUBHighlight) -> String {
let style = styleDescription(for: highlight)
let chapter = sectionTitleProvider(highlight)
let note = normalizedNote(highlight.note)
let parts = [style, chapter, note].compactMap { $0 }
if !parts.isEmpty {
return parts.joined(separator: "\n")
}
return highlight.location.href
}
private func titleText(for highlight: RDEPUBHighlight) -> String {
let text = highlight.text.trimmingCharacters(in: .whitespacesAndNewlines)
if highlight.hasNote {
return "批注: \(text)"
}
return text
}
private func styleDescription(for highlight: RDEPUBHighlight) -> String {
switch highlight.style {
case .highlight:
return highlight.hasNote ? "高亮批注" : "高亮"
case .underline:
return highlight.hasNote ? "划线批注" : "划线"
}
}
private func presentActions(for highlight: RDEPUBHighlight, sourceIndexPath: IndexPath) {
let alert = UIAlertController(title: "标注管理", message: nil, preferredStyle: .actionSheet)
alert.addAction(UIAlertAction(title: "跳转到位置", style: .default) { [weak self] _ in
self?.onSelectHighlight?(highlight)
})
alert.addAction(UIAlertAction(title: "编辑批注", style: .default) { [weak self] _ in
self?.presentNoteEditor(for: highlight)
})
alert.addAction(UIAlertAction(title: "删除标注", style: .destructive) { [weak self] _ in
self?.deleteHighlight(highlight)
})
alert.addAction(UIAlertAction(title: "取消", style: .cancel))
if let popover = alert.popoverPresentationController,
let cell = tableView.cellForRow(at: sourceIndexPath) {
popover.sourceView = cell
popover.sourceRect = cell.bounds
}
present(alert, animated: true)
}
private func presentNoteEditor(for highlight: RDEPUBHighlight) {
let alert = UIAlertController(title: "编辑批注", message: nil, preferredStyle: .alert)
alert.addTextField { textField in
textField.placeholder = "输入批注内容"
textField.text = highlight.note
}
alert.addAction(UIAlertAction(title: "取消", style: .cancel))
alert.addAction(UIAlertAction(title: "保存", style: .default) { [weak self, weak alert] _ in
guard let self,
let note = alert?.textFields?.first?.text,
let index = self.highlights.firstIndex(where: { $0.id == highlight.id }) else {
return
}
var updated = highlight
updated.note = self.normalizedNote(note)
self.highlights[index] = updated
self.tableView.reloadData()
self.onUpdateHighlight?(updated)
self.updateEmptyState()
})
present(alert, animated: true)
}
private func deleteHighlight(_ highlight: RDEPUBHighlight) {
guard let index = highlights.firstIndex(where: { $0.id == highlight.id }) else { return }
highlights.remove(at: index)
tableView.reloadData()
onDeleteHighlight?(highlight)
updateEmptyState()
}
private func normalizedNote(_ note: String?) -> String? {
let trimmed = note?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
return trimmed.isEmpty ? nil : trimmed
}
}
// MARK: -
/// EPUB
///
final class RDEPUBReaderBookmarksViewController: UITableViewController {
///
var onSelectBookmark: ((RDEPUBBookmark) -> Void)?
///
var onDeleteBookmark: ((RDEPUBBookmark) -> Void)?
private var bookmarks: [RDEPUBBookmark]
private let theme: RDEPUBReaderTheme
private let dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:mm"
return formatter
}()
init(bookmarks: [RDEPUBBookmark], theme: RDEPUBReaderTheme) {
self.bookmarks = bookmarks.sorted { $0.createdAt > $1.createdAt }
self.theme = theme
super.init(style: .plain)
title = "书签"
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
view.accessibilityIdentifier = "epub.reader.bookmarks.panel"
tableView.accessibilityIdentifier = "epub.reader.bookmarks.table"
tableView.tableFooterView = UIView(frame: .zero)
tableView.separatorInset = UIEdgeInsets(top: 0, left: 16, bottom: 0, right: 16)
applyTheme()
updateEmptyState()
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
bookmarks.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cellIdentifier = "BookmarkCell"
let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier) ?? UITableViewCell(style: .subtitle, reuseIdentifier: cellIdentifier)
let bookmark = bookmarks[indexPath.row]
cell.backgroundColor = theme.contentBackgroundColor
cell.textLabel?.textColor = theme.contentTextColor
cell.textLabel?.font = UIFont.systemFont(ofSize: 15, weight: .medium)
cell.textLabel?.numberOfLines = 2
cell.textLabel?.text = titleText(for: bookmark)
cell.detailTextLabel?.textColor = theme.contentTextColor.withAlphaComponent(0.7)
cell.detailTextLabel?.font = UIFont.systemFont(ofSize: 12)
cell.detailTextLabel?.numberOfLines = 3
cell.detailTextLabel?.text = detailText(for: bookmark)
cell.accessoryType = .disclosureIndicator
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
presentActions(for: bookmarks[indexPath.row], sourceIndexPath: indexPath)
}
private func applyTheme() {
tableView.backgroundColor = theme.contentBackgroundColor
navigationController?.navigationBar.tintColor = theme.toolControlTextColor
navigationController?.navigationBar.barTintColor = theme.toolBackgroundColor
navigationController?.navigationBar.titleTextAttributes = [.foregroundColor: theme.toolControlTextColor]
navigationController?.navigationBar.accessibilityIdentifier = "epub.reader.bookmarks.navbar"
}
private func updateEmptyState() {
guard bookmarks.isEmpty == false else {
let label = UILabel()
label.text = "暂无书签"
label.textAlignment = .center
label.textColor = theme.contentTextColor.withAlphaComponent(0.7)
label.numberOfLines = 0
tableView.backgroundView = label
return
}
tableView.backgroundView = nil
}
private func titleText(for bookmark: RDEPUBBookmark) -> String {
if let chapterTitle = bookmark.chapterTitle, !chapterTitle.isEmpty {
return chapterTitle
}
return bookmark.location.href
}
private func detailText(for bookmark: RDEPUBBookmark) -> String {
let parts = [
dateFormatter.string(from: bookmark.createdAt),
bookmark.note?.trimmingCharacters(in: .whitespacesAndNewlines),
bookmark.location.href
].compactMap { value -> String? in
guard let value, !value.isEmpty else { return nil }
return value
}
return parts.joined(separator: "\n")
}
private func presentActions(for bookmark: RDEPUBBookmark, sourceIndexPath: IndexPath) {
let alert = UIAlertController(title: "书签管理", message: nil, preferredStyle: .actionSheet)
alert.addAction(UIAlertAction(title: "跳转到位置", style: .default) { [weak self] _ in
self?.onSelectBookmark?(bookmark)
})
alert.addAction(UIAlertAction(title: "删除书签", style: .destructive) { [weak self] _ in
self?.deleteBookmark(bookmark)
})
alert.addAction(UIAlertAction(title: "取消", style: .cancel))
if let popover = alert.popoverPresentationController,
let cell = tableView.cellForRow(at: sourceIndexPath) {
popover.sourceView = cell
popover.sourceRect = cell.bounds
}
present(alert, animated: true)
}
private func deleteBookmark(_ bookmark: RDEPUBBookmark) {
guard let index = bookmarks.firstIndex(where: { $0.id == bookmark.id }) else { return }
bookmarks.remove(at: index)
tableView.reloadData()
onDeleteBookmark?(bookmark)
updateEmptyState()
}
}