- 新增 RDEPUBResourceDataProvider 解密钩子协议 + 访问登记表,收敛章节 HTML/ 图片/内联 CSS/脚注/封面/图片查看器/正文取图等读取点,scheme handler 对加密书 禁用流式分支;新增 RDEPUBDecryptingImageAttachment 解密 DTCoreText 图片附件; RDEPUBReaderDependencies.live(resourceDataProvider:) 便捷注入。明文书零影响。 - 试读墙:configuration.trialPolicy + delegate epubReaderTrialWallView/ DidReachTrialWall,UI 由宿主提供(RDEPUBReaderController+Trial)。 - 工具栏定制:RDEPUBReaderTop/BottomToolViewProtocol 协议 + dependencies 工厂注入, 内置栏已 conform,configureTopToolView 改协议类型。 - 朝向锁定:RDEPUBMetadata.orientation(OPF rendition:orientation 主, iBooks display-options 兜底)+ RDEPUBReaderController+Orientation 重写支持朝向、 打开后主动转向。 注:RDEPUBReaderController+LocationResolution / PaginationCoordinator 为本次改动前 即存在的工作区修改,一并纳入。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
236 lines
9.3 KiB
Swift
236 lines
9.3 KiB
Swift
import UIKit
|
|
|
|
final class RDEPUBReaderChromeCoordinator: NSObject, UIAdaptivePresentationControllerDelegate {
|
|
|
|
private unowned let context: RDEPUBReaderContext
|
|
|
|
init(context: RDEPUBReaderContext) {
|
|
self.context = context
|
|
}
|
|
|
|
private var controller: RDEPUBReaderController? {
|
|
context.controller
|
|
}
|
|
|
|
func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
|
|
|
|
context.runtime?.settingsPanelDidDisappear()
|
|
}
|
|
|
|
func makeTopToolView() -> RDEPUBReaderTopToolViewProviding {
|
|
let toolView: RDEPUBReaderTopToolViewProviding =
|
|
context.controller?.dependencies.makeTopToolView?() ?? RDEPUBReaderTopToolView()
|
|
toolView.onBack = { [weak self] in
|
|
self?.handleBackAction()
|
|
}
|
|
toolView.onSearch = { [weak self] in
|
|
self?.toggleSearchBar()
|
|
}
|
|
toolView.onToggleBookmark = { [weak self] in
|
|
_ = self?.context.runtime?.toggleBookmark()
|
|
}
|
|
return toolView
|
|
}
|
|
|
|
func makeBottomToolView() -> RDEPUBReaderBottomToolViewProviding {
|
|
let toolView: RDEPUBReaderBottomToolViewProviding =
|
|
context.controller?.dependencies.makeBottomToolView?() ?? RDEPUBReaderBottomToolView()
|
|
toolView.onShowTableOfContents = { [weak self] in
|
|
self?.presentTableOfContents()
|
|
}
|
|
toolView.onShowBookmarks = { [weak self] in
|
|
self?.context.runtime?.presentBookmarksManager()
|
|
}
|
|
toolView.onShowHighlights = { [weak self] in
|
|
self?.context.runtime?.presentHighlightsManager()
|
|
}
|
|
toolView.onAddHighlight = { [weak self] in
|
|
self?.context.runtime?.presentAnnotationCreation()
|
|
}
|
|
toolView.onShowSettings = { [weak self] in
|
|
self?.presentSettings()
|
|
}
|
|
return toolView
|
|
}
|
|
|
|
func updateReaderChrome() {
|
|
guard let controller else { return }
|
|
let uiState = makeUIState()
|
|
applyUIState(uiState)
|
|
updateSearchBar()
|
|
}
|
|
|
|
func makeUIState() -> RDEPUBReaderUIState {
|
|
guard let controller else { return .empty }
|
|
return RDEPUBReaderUIState(
|
|
canToggleBookmark: controller.currentBookIdentifier != nil,
|
|
hasBookmarkAtCurrentLocation: hasBookmarkAtCurrentLocation(),
|
|
canShowBookmarks: !controller.activeBookmarks.isEmpty,
|
|
canAddHighlight: controller.configuration.allowsHighlights && context.selectionState.hasSelection,
|
|
canShowHighlights: controller.configuration.allowsHighlights && !controller.activeHighlights.isEmpty,
|
|
showsTableOfContents: controller.configuration.showsTableOfContents,
|
|
allowsHighlights: controller.configuration.allowsHighlights,
|
|
showsSettingsPanel: controller.configuration.showsSettingsPanel
|
|
)
|
|
}
|
|
|
|
func applyUIState(_ state: RDEPUBReaderUIState) {
|
|
guard let controller else { return }
|
|
controller.topToolView.apply(theme: controller.configuration.theme)
|
|
controller.topToolView.setTitle(
|
|
controller.title
|
|
?? controller.parser?.metadata.title
|
|
?? controller.epubURL.deletingPathExtension().lastPathComponent
|
|
)
|
|
controller.topToolView.setBookmarkEnabled(state.canToggleBookmark)
|
|
controller.topToolView.setBookmarkSelected(state.hasBookmarkAtCurrentLocation)
|
|
controller.bottomToolView.apply(theme: controller.configuration.theme)
|
|
controller.bottomToolView.updateVisibility(
|
|
showsTableOfContents: state.showsTableOfContents,
|
|
allowsHighlights: state.allowsHighlights,
|
|
showsSettingsPanel: state.showsSettingsPanel
|
|
)
|
|
controller.bottomToolView.setBookmarksEnabled(state.canShowBookmarks)
|
|
controller.bottomToolView.setAddHighlightEnabled(state.canAddHighlight)
|
|
controller.bottomToolView.setHighlightsEnabled(state.canShowHighlights)
|
|
}
|
|
|
|
private func hasBookmarkAtCurrentLocation() -> Bool {
|
|
guard let controller else { return false }
|
|
return context.runtime?.annotationCoordinator.currentBookmark() != nil
|
|
}
|
|
|
|
func presentSettings() {
|
|
guard let controller else { return }
|
|
guard controller.configuration.showsSettingsPanel else { return }
|
|
|
|
context.runtime?.settingsPanelWillAppear()
|
|
|
|
let settingsController = RDEPUBReaderSettingsViewController(
|
|
configuration: controller.configuration,
|
|
brightness: controller.currentBrightness
|
|
)
|
|
settingsController.onBrightnessChange = { [weak controller] brightness in
|
|
controller?.setScreenBrightness(brightness)
|
|
}
|
|
settingsController.onFontSizeChange = { [weak controller] fontSize in
|
|
controller?.updateConfiguration { $0.fontSize = fontSize }
|
|
}
|
|
settingsController.onFontChoiceChange = { [weak controller] fontChoice in
|
|
controller?.updateConfiguration { $0.fontChoice = fontChoice }
|
|
}
|
|
settingsController.onLineHeightChange = { [weak controller] lineHeightMultiple in
|
|
controller?.updateConfiguration { $0.lineHeightMultiple = lineHeightMultiple }
|
|
}
|
|
settingsController.onColumnCountChange = { [weak controller] numberOfColumns in
|
|
controller?.updateConfiguration { $0.numberOfColumns = numberOfColumns }
|
|
}
|
|
settingsController.onDisplayTypeChange = { [weak controller] displayType in
|
|
controller?.updateConfiguration { $0.displayType = displayType }
|
|
}
|
|
settingsController.onThemeChange = { [weak controller] theme in
|
|
controller?.updateConfiguration { $0.theme = theme }
|
|
}
|
|
settingsController.onDismiss = { [weak self] in
|
|
|
|
self?.context.runtime?.settingsPanelDidDisappear()
|
|
}
|
|
|
|
let navigationController = UINavigationController(rootViewController: settingsController)
|
|
navigationController.modalPresentationStyle = .pageSheet
|
|
navigationController.presentationController?.delegate = self
|
|
controller.present(navigationController, animated: true)
|
|
}
|
|
|
|
func presentTableOfContents() {
|
|
guard let controller else { return }
|
|
guard controller.configuration.showsTableOfContents else { return }
|
|
let items = controller.flattenedTableOfContentsItems(
|
|
from: controller.publication?.tableOfContents ?? [],
|
|
includePageNumbers: false
|
|
)
|
|
guard !items.isEmpty else { return }
|
|
|
|
let chapterController = RDEPUBReaderChapterListController(
|
|
items: items,
|
|
currentItem: controller.currentTableOfContentsItem,
|
|
theme: controller.configuration.theme
|
|
)
|
|
chapterController.onSelectItem = { [weak controller] item in
|
|
guard let controller else { return }
|
|
chapterController.dismiss(animated: true) {
|
|
_ = controller.go(toTableOfContentsItem: item, animated: true)
|
|
}
|
|
}
|
|
|
|
let navigationController = UINavigationController(rootViewController: chapterController)
|
|
navigationController.modalPresentationStyle = .pageSheet
|
|
controller.present(navigationController, animated: true)
|
|
}
|
|
|
|
func toggleSearchBar() {
|
|
guard let controller else { return }
|
|
if controller.isSearchBarVisible {
|
|
controller.hideSearchBar()
|
|
} else {
|
|
controller.showSearchBar()
|
|
}
|
|
}
|
|
|
|
func updateSearchBar() {
|
|
guard let controller else { return }
|
|
controller.searchBarView.apply(theme: controller.configuration.theme)
|
|
if let searchState = controller.searchState {
|
|
if let index = searchState.currentMatchIndex {
|
|
controller.searchBarView.updateMatchCount(current: index + 1, total: searchState.matches.count)
|
|
} else if searchState.matches.isEmpty {
|
|
controller.searchBarView.showNoResults()
|
|
}
|
|
}
|
|
}
|
|
|
|
func handleBackAction() {
|
|
guard let controller else { return }
|
|
close(controller)
|
|
}
|
|
|
|
private func close(_ controller: UIViewController) {
|
|
let target = closestDismissTarget(from: controller)
|
|
if let navigationController = target.navigationController,
|
|
navigationController.viewControllers.first !== target {
|
|
navigationController.popViewController(animated: true)
|
|
return
|
|
}
|
|
|
|
if let navigationController = target.navigationController,
|
|
navigationController.presentingViewController != nil {
|
|
navigationController.dismiss(animated: true)
|
|
return
|
|
}
|
|
|
|
if target.presentingViewController != nil {
|
|
target.dismiss(animated: true)
|
|
return
|
|
}
|
|
|
|
controller.dismiss(animated: true)
|
|
}
|
|
|
|
private func closestDismissTarget(from controller: UIViewController) -> UIViewController {
|
|
var candidate: UIViewController = controller
|
|
var current = controller.parent
|
|
while let parent = current {
|
|
if let navigationController = parent.navigationController,
|
|
navigationController.viewControllers.contains(parent) {
|
|
return parent
|
|
}
|
|
if parent.presentingViewController != nil || parent.navigationController?.presentingViewController != nil {
|
|
return parent
|
|
}
|
|
candidate = parent
|
|
current = parent.parent
|
|
}
|
|
return candidate
|
|
}
|
|
}
|