import UIKit 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() view.accessibilityIdentifier = "epub.reader.highlights.panel" tableView.accessibilityIdentifier = "epub.reader.highlights.table" 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.accessibilityIdentifier = "epub.reader.highlights.filter" 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] navigationController?.navigationBar.accessibilityIdentifier = "epub.reader.highlights.navbar" } 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 label.accessibilityIdentifier = "epub.reader.highlights.empty" 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 } } 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 label.accessibilityIdentifier = "epub.reader.bookmarks.empty" 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() } }