ReadViewSDK/Sources/RDReaderView/EPUBUI/RDEPUBReaderHighlightsViewController.swift
shen 54798ba578 refactor: 添加中文注释 + 优化模块结构
- 给全部 78 个 Swift 源文件添加详细的中文注释(文件级、类级、方法级)
- 删除 LegacyRDReaderController/ 死代码目录(16 文件 4592 行)
- 根目录翻页容器文件移入 ReaderView/ 目录
- Resources/ 移入 EPUBCore/Resources/(与使用者归属一致)
- RDEPUBTextIndexTable.swift 移入 EPUBTextRendering/(消除反向依赖)
- RDURLReaderController.swift 移入 EPUBUI/(入口控制器归入 UI 层)
- 更新 podspec 资源路径
2026-05-25 10:19:14 +08:00

350 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()
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]
}
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()
}
}