修复阅读器资源、朗读与持久化稳定性

阅读器在多会话朗读、加密 EPUB 资源、并发打开同一本书和多 UserDefaults 容器等场景下,存在旧异步结果覆盖新状态、锁屏控制串会话、大型明文资源被误拒绝、解压半成品被复用及标注跨容器混用的风险;同时高亮、书签、搜索和设置面板的空状态与自动化可访问性入口不完整。\n\n本次为朗读会话引入代次和当前 utterance 校验,远程控制改为仅响应当前会话并按自身 token 清理;加密资源 provider 先判定是否实际返回解密数据,明文大资源继续流式读取;解压缓存串行化以避免并发复用未完成目录。书签与高亮迁移至受保护文件,按 UserDefaults 容器隔离命名空间,保留标准容器旧文件兼容并确保新副本成功落盘后才删除历史数据。\n\n同步调整缓存淘汰、FoundationModels 弱链接、搜索/标注/设置面板交互与 UI 测试,并更新 Pod 生成配置及 API、风险文档。已执行 git diff --check 和 ReadViewDemo Debug Simulator 构建,结果为 BUILD SUCCEEDED。
This commit is contained in:
shenlei
2026-07-30 20:52:21 +09:00
parent e01cbc169d
commit 3e60bf1869
40 changed files with 3210 additions and 2691 deletions
@@ -16,6 +16,10 @@ final class RDEPUBReaderHighlightsViewController: UITableViewController {
private let filterControl = UISegmentedControl(items: ["全部", "注释", "划线"])
/// tableView `backgroundView`
/// backgroundView VoiceOver UI
private let emptyStateLabel = UILabel()
private var filteredHighlights: [RDEPUBHighlight] {
switch filterControl.selectedSegmentIndex {
case 1:
@@ -120,19 +124,28 @@ final class RDEPUBReaderHighlightsViewController: UITableViewController {
navigationController?.navigationBar.accessibilityIdentifier = "epub.reader.highlights.navbar"
}
private func updateEmptyState() {
guard filteredHighlights.isEmpty else {
tableView.backgroundView = nil
return
}
private func installEmptyStateLabelIfNeeded() {
guard emptyStateLabel.superview == nil else { return }
emptyStateLabel.textAlignment = .center
emptyStateLabel.numberOfLines = 0
emptyStateLabel.translatesAutoresizingMaskIntoConstraints = false
emptyStateLabel.accessibilityIdentifier = "epub.reader.highlights.empty"
emptyStateLabel.isAccessibilityElement = true
tableView.addSubview(emptyStateLabel)
//
NSLayoutConstraint.activate([
emptyStateLabel.centerXAnchor.constraint(equalTo: tableView.frameLayoutGuide.centerXAnchor),
emptyStateLabel.centerYAnchor.constraint(equalTo: tableView.frameLayoutGuide.centerYAnchor),
emptyStateLabel.leadingAnchor.constraint(greaterThanOrEqualTo: tableView.frameLayoutGuide.leadingAnchor, constant: 24),
emptyStateLabel.trailingAnchor.constraint(lessThanOrEqualTo: tableView.frameLayoutGuide.trailingAnchor, constant: -24)
])
}
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
private func updateEmptyState() {
installEmptyStateLabelIfNeeded()
emptyStateLabel.textColor = theme.contentTextColor.withAlphaComponent(0.7)
emptyStateLabel.text = emptyStateText()
emptyStateLabel.isHidden = !filteredHighlights.isEmpty
}
@objc private func filterChangedAction() {
@@ -209,6 +222,9 @@ final class RDEPUBReaderBookmarksViewController: UITableViewController {
return formatter
}()
/// highlights `tableView.backgroundView`
private let emptyStateLabel = UILabel()
init(bookmarks: [RDEPUBBookmark], theme: RDEPUBReaderTheme) {
self.bookmarks = bookmarks.sorted { $0.createdAt > $1.createdAt }
self.theme = theme
@@ -279,18 +295,27 @@ final class RDEPUBReaderBookmarksViewController: UITableViewController {
navigationController?.navigationBar.accessibilityIdentifier = "epub.reader.bookmarks.navbar"
}
private func installEmptyStateLabelIfNeeded() {
guard emptyStateLabel.superview == nil else { return }
emptyStateLabel.text = "暂无书签"
emptyStateLabel.textAlignment = .center
emptyStateLabel.numberOfLines = 0
emptyStateLabel.translatesAutoresizingMaskIntoConstraints = false
emptyStateLabel.accessibilityIdentifier = "epub.reader.bookmarks.empty"
emptyStateLabel.isAccessibilityElement = true
tableView.addSubview(emptyStateLabel)
NSLayoutConstraint.activate([
emptyStateLabel.centerXAnchor.constraint(equalTo: tableView.frameLayoutGuide.centerXAnchor),
emptyStateLabel.centerYAnchor.constraint(equalTo: tableView.frameLayoutGuide.centerYAnchor),
emptyStateLabel.leadingAnchor.constraint(greaterThanOrEqualTo: tableView.frameLayoutGuide.leadingAnchor, constant: 24),
emptyStateLabel.trailingAnchor.constraint(lessThanOrEqualTo: tableView.frameLayoutGuide.trailingAnchor, constant: -24)
])
}
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
installEmptyStateLabelIfNeeded()
emptyStateLabel.textColor = theme.contentTextColor.withAlphaComponent(0.7)
emptyStateLabel.isHidden = !bookmarks.isEmpty
}
private func titleText(for bookmark: RDEPUBBookmark) -> String {
@@ -1,3 +1,4 @@
import CryptoKit
import Foundation
public protocol RDEPUBReaderPersistence: AnyObject {
@@ -47,6 +48,113 @@ public extension RDEPUBReaderPersistence {
}
}
///
/// UserDefaults Application Support
/// `NSFileProtection` iCloud/iTunes
public final class RDEPUBProtectedContentStore {
private let directoryURL: URL
private let protection: FileProtectionType
private let fileManager = FileManager.default
private let namespace: String?
private let allowsLegacyFallback: Bool
/// `.completeUntilFirstUserAuthentication`访
/// `.complete` `.complete`
/// 退
/// 宿 `.complete`
public init(
directoryName: String = "ssreaderview-epub/protected",
namespace: String? = nil,
allowsLegacyFallback: Bool = true,
protection: FileProtectionType = .completeUntilFirstUserAuthentication
) {
let base = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first
?? FileManager.default.temporaryDirectory
self.directoryURL = base.appendingPathComponent(directoryName, isDirectory: true)
self.namespace = namespace
self.allowsLegacyFallback = allowsLegacyFallback
self.protection = protection
}
/// bookIdentifier `/`
/// 255 SHA256
private func fileURL(forKey key: String) -> URL {
let storageKey: String
if let namespace {
storageKey = namespace + "\u{0}" + key
} else {
storageKey = key
}
let digest = SHA256.hash(data: Data(storageKey.utf8))
let name = digest.map { String(format: "%02x", $0) }.joined()
return directoryURL.appendingPathComponent("\(name).json", isDirectory: false)
}
private func legacyFileURL(forKey key: String) -> URL {
let digest = SHA256.hash(data: Data(key.utf8))
let name = digest.map { String(format: "%02x", $0) }.joined()
return directoryURL.appendingPathComponent("\(name).json", isDirectory: false)
}
private func ensureDirectory() throws {
guard !fileManager.fileExists(atPath: directoryURL.path) else { return }
try fileManager.createDirectory(
at: directoryURL,
withIntermediateDirectories: true,
attributes: [.protectionKey: protection]
)
var url = directoryURL
var values = URLResourceValues()
values.isExcludedFromBackup = true
try? url.setResourceValues(values)
}
public func read(forKey key: String) -> Data? {
if let data = try? Data(contentsOf: fileURL(forKey: key)) {
return data
}
guard namespace != nil, allowsLegacyFallback else { return nil }
return try? Data(contentsOf: legacyFileURL(forKey: key))
}
public func write(_ data: Data, forKey key: String) {
_ = writeIfPossible(data, forKey: key)
}
///
@discardableResult
func writeIfPossible(_ data: Data, forKey key: String) -> Bool {
do {
try ensureDirectory()
try data.write(to: fileURL(forKey: key), options: .atomic)
// atomic
try fileManager.setAttributes([.protectionKey: protection], ofItemAtPath: fileURL(forKey: key).path)
return true
} catch {
#if DEBUG
print("[RDEPUBProtectedContentStore] ⚠️ Failed to write '\(key)': \(error)")
#endif
return false
}
}
public func removeValue(forKey key: String) {
try? fileManager.removeItem(at: fileURL(forKey: key))
if namespace != nil, allowsLegacyFallback {
try? fileManager.removeItem(at: legacyFileURL(forKey: key))
}
}
public func removeAll() {
try? fileManager.removeItem(at: directoryURL)
}
}
public final class RDEPUBUserDefaultsPersistence: RDEPUBReaderPersistence {
private let defaults: UserDefaults
@@ -59,18 +167,80 @@ public final class RDEPUBUserDefaultsPersistence: RDEPUBReaderPersistence {
private let settingsKey: String
///
private let contentStore: RDEPUBProtectedContentStore
public init(
defaults: UserDefaults = .standard,
locationPrefix: String = "ssreader.epub.location.",
bookmarksPrefix: String = "ssreader.epub.bookmarks.",
highlightsPrefix: String = "ssreader.epub.highlights.",
settingsKey: String = "ssreader.epub.settings"
settingsKey: String = "ssreader.epub.settings",
contentStore: RDEPUBProtectedContentStore? = nil
) {
self.defaults = defaults
self.locationPrefix = locationPrefix
self.bookmarksPrefix = bookmarksPrefix
self.highlightsPrefix = highlightsPrefix
self.settingsKey = settingsKey
if let contentStore {
self.contentStore = contentStore
} else {
let namespaceKey = settingsKey + ".protectedContentNamespace"
let isStandardDefaults = defaults === UserDefaults.standard
let namespace: String
if isStandardDefaults {
namespace = "standard"
} else if let storedNamespace = defaults.string(forKey: namespaceKey) {
namespace = storedNamespace
} else {
namespace = UUID().uuidString
defaults.set(namespace, forKey: namespaceKey)
}
self.contentStore = RDEPUBProtectedContentStore(
namespace: namespace,
// suite
allowsLegacyFallback: isStandardDefaults
)
}
}
/// 访
private func loadContentData(key: String) -> Data? {
if let data = contentStore.read(forKey: key) {
return data
}
guard let legacy = defaults.data(forKey: key) else { return nil }
if contentStore.writeIfPossible(legacy, forKey: key) {
defaults.removeObject(forKey: key)
}
return legacy
}
private func saveContentData(_ data: Data, key: String) {
//
if contentStore.writeIfPossible(data, forKey: key),
defaults.object(forKey: key) != nil {
defaults.removeObject(forKey: key)
}
}
///
public func clearReadingData(for bookIdentifier: String) {
contentStore.removeValue(forKey: highlightsPrefix + bookIdentifier)
contentStore.removeValue(forKey: bookmarksPrefix + bookIdentifier)
defaults.removeObject(forKey: highlightsPrefix + bookIdentifier)
defaults.removeObject(forKey: bookmarksPrefix + bookIdentifier)
defaults.removeObject(forKey: locationPrefix + bookIdentifier)
}
/// UserDefaults
public func clearAllReadingContent() {
contentStore.removeAll()
for key in defaults.dictionaryRepresentation().keys
where key.hasPrefix(highlightsPrefix) || key.hasPrefix(bookmarksPrefix) {
defaults.removeObject(forKey: key)
}
}
public func loadLocation(for bookIdentifier: String) -> RDEPUBLocation? {
@@ -99,7 +269,7 @@ public final class RDEPUBUserDefaultsPersistence: RDEPUBReaderPersistence {
}
public func loadBookmarks(for bookIdentifier: String) -> [RDEPUBBookmark] {
guard let data = defaults.data(forKey: bookmarksPrefix + bookIdentifier) else {
guard let data = loadContentData(key: bookmarksPrefix + bookIdentifier) else {
return []
}
do {
@@ -115,7 +285,7 @@ public final class RDEPUBUserDefaultsPersistence: RDEPUBReaderPersistence {
public func saveBookmarks(_ bookmarks: [RDEPUBBookmark], for bookIdentifier: String) {
do {
let data = try JSONEncoder().encode(bookmarks)
defaults.set(data, forKey: bookmarksPrefix + bookIdentifier)
saveContentData(data, key: bookmarksPrefix + bookIdentifier)
} catch {
#if DEBUG
print("[RDEPUBUserDefaultsPersistence] ⚠️ Failed to encode bookmarks for '\(bookIdentifier)': \(error)")
@@ -124,7 +294,7 @@ public final class RDEPUBUserDefaultsPersistence: RDEPUBReaderPersistence {
}
public func loadHighlights(for bookIdentifier: String) -> [RDEPUBHighlight] {
guard let data = defaults.data(forKey: highlightsPrefix + bookIdentifier) else {
guard let data = loadContentData(key: highlightsPrefix + bookIdentifier) else {
return []
}
do {
@@ -145,7 +315,7 @@ public final class RDEPUBUserDefaultsPersistence: RDEPUBReaderPersistence {
print("[RDEPUBUserDefaultsPersistence] ⚠️ saveHighlights data size (\(data.count) bytes) exceeds 1MB for: \(bookIdentifier)")
#endif
}
defaults.set(data, forKey: highlightsPrefix + bookIdentifier)
saveContentData(data, key: highlightsPrefix + bookIdentifier)
} catch {
#if DEBUG
print("[RDEPUBUserDefaultsPersistence] ⚠️ Failed to encode highlights for '\(bookIdentifier)': \(error)")
@@ -65,12 +65,18 @@ final class RDEPUBReaderSearchBarView: RDEPUBReaderToolView {
private let emptyStateLabel = UILabel()
private let navigationRowView = UIView()
private let previousButton = RDEPUBReaderTintButton(type: .system)
private let nextButton = RDEPUBReaderTintButton(type: .system)
private let countLabel = UILabel()
/// Collapsed to 0 in the pristine state so the prompt keeps its original
/// spacing under the search field. See `updateNavigationRow(total:)`.
private var navigationRowHeightConstraint: NSLayoutConstraint?
private var searchSections: [RDEPUBReaderSearchSection] = []
private var keyword = ""
@@ -85,7 +91,7 @@ final class RDEPUBReaderSearchBarView: RDEPUBReaderToolView {
setupSubviews()
setupConstraints()
setupActions()
updateLegacyNavigationEnabled(false)
updateNavigationRow(total: 0)
showInitialState()
}
@@ -94,20 +100,14 @@ final class RDEPUBReaderSearchBarView: RDEPUBReaderToolView {
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
// Only claim touches that fall within the panelView or the
// accessibility-only navigation buttons. Taps on the dimmed scrim
// area pass through to the reader content below, allowing chrome
// toggle taps to work.
// Only claim touches that fall within the panelView. Taps on the dimmed
// scrim area pass through to the reader content below, allowing chrome
// toggle taps to work. The step controls live inside the panel, so they
// no longer need a special case here.
let panelPoint = convert(point, to: panelView)
if panelView.point(inside: panelPoint, with: event) {
return super.hitTest(point, with: event)
}
// Also claim touches on the accessibility-only navigation buttons
if previousButton.frame.contains(point)
|| nextButton.frame.contains(point)
|| countLabel.frame.contains(point) {
return super.hitTest(point, with: event)
}
return nil
}
@@ -149,9 +149,10 @@ final class RDEPUBReaderSearchBarView: RDEPUBReaderToolView {
tableView.backgroundColor = .clear
tableView.separatorStyle = .none
navigationRowView.backgroundColor = rowColor
previousButton.tintColor = textColor
nextButton.tintColor = textColor
countLabel.textColor = textColor
countLabel.textColor = secondaryTextColor
countLabel.backgroundColor = .clear
RDEPUBReaderSearchResultCell.cardBackgroundColor = cardColor
@@ -168,9 +169,11 @@ final class RDEPUBReaderSearchBarView: RDEPUBReaderToolView {
}
func updateMatchCount(current: Int, total: Int) {
// label "N/M" UI
// accessibilityLabel XCUIElement.label
countLabel.text = "\(current)/\(total)"
textField.accessibilityValue = "\(current)/\(total)"
updateLegacyNavigationEnabled(total > 0)
updateNavigationRow(total: total)
}
func showNoResults() {
@@ -223,6 +226,7 @@ final class RDEPUBReaderSearchBarView: RDEPUBReaderToolView {
private func setupSubviews() {
backgroundButton.translatesAutoresizingMaskIntoConstraints = false
panelView.translatesAutoresizingMaskIntoConstraints = false
navigationRowView.translatesAutoresizingMaskIntoConstraints = false
grabberView.translatesAutoresizingMaskIntoConstraints = false
searchRowView.translatesAutoresizingMaskIntoConstraints = false
searchFieldContainer.translatesAutoresizingMaskIntoConstraints = false
@@ -241,6 +245,7 @@ final class RDEPUBReaderSearchBarView: RDEPUBReaderToolView {
panelView.addSubview(grabberView)
panelView.addSubview(searchRowView)
panelView.addSubview(navigationRowView)
panelView.addSubview(tableView)
panelView.addSubview(emptyStateLabel)
@@ -250,14 +255,15 @@ final class RDEPUBReaderSearchBarView: RDEPUBReaderToolView {
searchFieldContainer.addSubview(searchIcon)
searchFieldContainer.addSubview(textField)
addSubview(previousButton)
addSubview(nextButton)
addSubview(countLabel)
navigationRowView.addSubview(countLabel)
navigationRowView.addSubview(previousButton)
navigationRowView.addSubview(nextButton)
if #available(iOS 13.0, *) {
searchIcon.image = UIImage(systemName: "magnifyingglass")
previousButton.setImage(UIImage(systemName: "chevron.up"), for: .normal)
nextButton.setImage(UIImage(systemName: "chevron.down"), for: .normal)
let stepSymbol = UIImage.SymbolConfiguration(pointSize: 15, weight: .semibold)
previousButton.setImage(UIImage(systemName: "chevron.up", withConfiguration: stepSymbol), for: .normal)
nextButton.setImage(UIImage(systemName: "chevron.down", withConfiguration: stepSymbol), for: .normal)
} else {
previousButton.setTitle("", for: .normal)
nextButton.setTitle("", for: .normal)
@@ -287,11 +293,16 @@ final class RDEPUBReaderSearchBarView: RDEPUBReaderToolView {
nextButton.accessibilityIdentifier = "epub.reader.search.next"
countLabel.accessibilityIdentifier = "epub.reader.search.count"
textField.accessibilityIdentifier = "epub.reader.search.field"
// 0.02 0.01CALayer.opacity Float320.01
// 0.01 UIKit
previousButton.alpha = 0.02
nextButton.alpha = 0.02
countLabel.alpha = 0.02
previousButton.accessibilityLabel = "上一个匹配"
nextButton.accessibilityLabel = "下一个匹配"
countLabel.font = UIFont.systemFont(ofSize: 15, weight: .medium)
countLabel.textAlignment = .left
countLabel.isAccessibilityElement = true
navigationRowView.layer.cornerRadius = 18
navigationRowView.clipsToBounds = true
tableView.register(RDEPUBReaderSearchResultCell.self, forCellReuseIdentifier: RDEPUBReaderSearchResultCell.reuseIdentifier)
tableView.dataSource = self
@@ -345,33 +356,40 @@ final class RDEPUBReaderSearchBarView: RDEPUBReaderToolView {
cancelButton.trailingAnchor.constraint(equalTo: searchRowView.trailingAnchor, constant: -18),
cancelButton.centerYAnchor.constraint(equalTo: searchRowView.centerYAnchor),
navigationRowView.leadingAnchor.constraint(equalTo: panelView.leadingAnchor, constant: 20),
navigationRowView.trailingAnchor.constraint(equalTo: panelView.trailingAnchor, constant: -20),
navigationRowView.topAnchor.constraint(equalTo: searchRowView.bottomAnchor, constant: 12),
countLabel.leadingAnchor.constraint(equalTo: navigationRowView.leadingAnchor, constant: 16),
countLabel.centerYAnchor.constraint(equalTo: navigationRowView.centerYAnchor),
nextButton.trailingAnchor.constraint(equalTo: navigationRowView.trailingAnchor, constant: -8),
nextButton.centerYAnchor.constraint(equalTo: navigationRowView.centerYAnchor),
nextButton.widthAnchor.constraint(equalToConstant: 44),
nextButton.topAnchor.constraint(equalTo: navigationRowView.topAnchor),
nextButton.bottomAnchor.constraint(equalTo: navigationRowView.bottomAnchor),
previousButton.trailingAnchor.constraint(equalTo: nextButton.leadingAnchor, constant: -4),
previousButton.centerYAnchor.constraint(equalTo: navigationRowView.centerYAnchor),
previousButton.widthAnchor.constraint(equalToConstant: 44),
previousButton.topAnchor.constraint(equalTo: navigationRowView.topAnchor),
previousButton.bottomAnchor.constraint(equalTo: navigationRowView.bottomAnchor),
countLabel.trailingAnchor.constraint(lessThanOrEqualTo: previousButton.leadingAnchor, constant: -8),
tableView.leadingAnchor.constraint(equalTo: panelView.leadingAnchor, constant: 0),
tableView.trailingAnchor.constraint(equalTo: panelView.trailingAnchor, constant: 0),
tableView.topAnchor.constraint(equalTo: searchRowView.bottomAnchor, constant: 18),
tableView.topAnchor.constraint(equalTo: navigationRowView.bottomAnchor, constant: 12),
tableView.bottomAnchor.constraint(equalTo: panelView.safeAreaLayoutGuide.bottomAnchor),
emptyStateLabel.leadingAnchor.constraint(equalTo: panelView.leadingAnchor, constant: 32),
emptyStateLabel.trailingAnchor.constraint(equalTo: panelView.trailingAnchor, constant: -32),
emptyStateLabel.topAnchor.constraint(equalTo: searchRowView.bottomAnchor, constant: 56),
// /UI panel
//
//
previousButton.topAnchor.constraint(equalTo: panelView.topAnchor),
previousButton.leadingAnchor.constraint(equalTo: leadingAnchor),
previousButton.widthAnchor.constraint(equalToConstant: 1),
previousButton.heightAnchor.constraint(equalToConstant: 1),
nextButton.topAnchor.constraint(equalTo: panelView.topAnchor),
nextButton.leadingAnchor.constraint(equalTo: previousButton.trailingAnchor),
nextButton.widthAnchor.constraint(equalToConstant: 1),
nextButton.heightAnchor.constraint(equalToConstant: 1),
countLabel.topAnchor.constraint(equalTo: panelView.topAnchor),
countLabel.leadingAnchor.constraint(equalTo: nextButton.trailingAnchor),
countLabel.widthAnchor.constraint(equalToConstant: 1),
countLabel.heightAnchor.constraint(equalToConstant: 1)
emptyStateLabel.topAnchor.constraint(equalTo: navigationRowView.bottomAnchor, constant: 50)
])
let heightConstraint = navigationRowView.heightAnchor.constraint(equalToConstant: 0)
heightConstraint.isActive = true
navigationRowHeightConstraint = heightConstraint
}
private func setupActions() {
@@ -384,9 +402,18 @@ final class RDEPUBReaderSearchBarView: RDEPUBReaderToolView {
backgroundButton.addTarget(self, action: #selector(closeAction), for: .touchUpInside)
}
private func updateLegacyNavigationEnabled(_ enabled: Bool) {
previousButton.isEnabled = enabled
nextButton.isEnabled = enabled
/// Stepping is only enabled once there are matches, but the row stays
/// visible (showing `0/0` with disabled arrows) for any keyword the user has
/// actually searched, so the result count is never silently missing. It
/// collapses away only in the pristine "type a keyword" state.
private func updateNavigationRow(total: Int) {
let hasMatches = total > 0
previousButton.isEnabled = hasMatches
nextButton.isEnabled = hasMatches
let isVisible = hasMatches || !keyword.isEmpty
navigationRowView.isHidden = !isVisible
navigationRowHeightConstraint?.constant = isVisible ? 36 : 0
}
private func showInitialState() {
@@ -198,8 +198,7 @@ final class RDEPUBReaderAnnotationCoordinator {
func presentHighlightsManager() {
guard let controller else { return }
guard controller.configuration.allowsHighlights else { return }
guard !controller.activeHighlights.isEmpty else { return }
// presentBookmarksManager
let highlightsController = RDEPUBReaderHighlightsViewController(
highlights: controller.activeHighlights,
theme: controller.configuration.theme,
@@ -325,8 +324,8 @@ final class RDEPUBReaderAnnotationCoordinator {
func presentBookmarksManager() {
guard let controller else { return }
guard !controller.activeBookmarks.isEmpty else { return }
// 退
//
let bookmarksController = RDEPUBReaderBookmarksViewController(
bookmarks: controller.activeBookmarks,
theme: controller.configuration.theme