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.hasNote } 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.separatorStyle = .none tableView.contentInset = UIEdgeInsets(top: 12, left: 0, bottom: 20, right: 0) navigationItem.rightBarButtonItem = editButtonItem editButtonItem.title = "编辑" tableView.allowsMultipleSelectionDuringEditing = false 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 = .clear cell.contentView.backgroundColor = UIColor.white.withAlphaComponent(0.76) cell.contentView.layer.cornerRadius = 14 cell.contentView.layer.masksToBounds = true cell.textLabel?.textColor = UIColor(red: 0.08, green: 0.10, blue: 0.14, alpha: 1) cell.textLabel?.font = UIFont.systemFont(ofSize: 15, weight: .medium) cell.textLabel?.numberOfLines = 2 cell.textLabel?.text = titleText(for: highlight) cell.detailTextLabel?.textColor = UIColor(red: 0.34, green: 0.36, blue: 0.40, alpha: 1) cell.detailTextLabel?.font = UIFont.systemFont(ofSize: 12) cell.detailTextLabel?.numberOfLines = 3 cell.detailTextLabel?.text = detailText(for: highlight) cell.accessoryType = .none return cell } override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { 112 } override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { cell.contentView.frame = cell.bounds.inset(by: UIEdgeInsets(top: 6, left: 18, bottom: 6, right: 18)) } override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { guard !tableView.isEditing else { return } tableView.deselectRow(at: indexPath, animated: true) onSelectHighlight?(filteredHighlights[indexPath.row]) } override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { true } override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { guard editingStyle == .delete else { return } deleteHighlight(filteredHighlights[indexPath.row]) } 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 = UIColor(red: 0.975, green: 0.965, blue: 0.935, alpha: 1) navigationController?.navigationBar.tintColor = UIColor(red: 0.10, green: 0.34, blue: 0.78, alpha: 1) navigationController?.navigationBar.barTintColor = tableView.backgroundColor navigationController?.navigationBar.titleTextAttributes = [.foregroundColor: UIColor(red: 0.08, green: 0.10, blue: 0.14, alpha: 1)] 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 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.separatorStyle = .none tableView.contentInset = UIEdgeInsets(top: 12, left: 0, bottom: 20, right: 0) navigationItem.rightBarButtonItem = editButtonItem editButtonItem.title = "编辑" 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 = .clear cell.contentView.backgroundColor = UIColor.white.withAlphaComponent(0.76) cell.contentView.layer.cornerRadius = 14 cell.contentView.layer.masksToBounds = true cell.textLabel?.textColor = UIColor(red: 0.08, green: 0.10, blue: 0.14, alpha: 1) cell.textLabel?.font = UIFont.systemFont(ofSize: 15, weight: .medium) cell.textLabel?.numberOfLines = 2 cell.textLabel?.text = titleText(for: bookmark) cell.detailTextLabel?.textColor = UIColor(red: 0.34, green: 0.36, blue: 0.40, alpha: 1) cell.detailTextLabel?.font = UIFont.systemFont(ofSize: 12) cell.detailTextLabel?.numberOfLines = 3 cell.detailTextLabel?.text = detailText(for: bookmark) cell.accessoryType = .none return cell } override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { 104 } override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { guard editingStyle == .delete else { return } deleteBookmark(bookmarks[indexPath.row]) } override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: true) onSelectBookmark?(bookmarks[indexPath.row]) } private func applyTheme() { tableView.backgroundColor = UIColor(red: 0.975, green: 0.965, blue: 0.935, alpha: 1) navigationController?.navigationBar.tintColor = UIColor(red: 0.10, green: 0.34, blue: 0.78, alpha: 1) navigationController?.navigationBar.barTintColor = tableView.backgroundColor navigationController?.navigationBar.titleTextAttributes = [.foregroundColor: UIColor(red: 0.08, green: 0.10, blue: 0.14, alpha: 1)] 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 deleteBookmark(_ bookmark: RDEPUBBookmark) { guard let index = bookmarks.firstIndex(where: { $0.id == bookmark.id }) else { return } bookmarks.remove(at: index) tableView.reloadData() onDeleteBookmark?(bookmark) updateEmptyState() } }