feat: 支持加密 EPUB、试读墙、工具栏定制与朝向锁定

- 新增 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>
This commit is contained in:
shenlei
2026-07-10 12:25:17 +09:00
co-authored by Claude Fable 5
parent 83dfa40299
commit d5a7755702
30 changed files with 2218 additions and 1569 deletions
@@ -17,8 +17,9 @@ final class RDEPUBReaderChromeCoordinator: NSObject, UIAdaptivePresentationContr
context.runtime?.settingsPanelDidDisappear()
}
func makeTopToolView() -> RDEPUBReaderTopToolView {
let toolView = RDEPUBReaderTopToolView()
func makeTopToolView() -> RDEPUBReaderTopToolViewProviding {
let toolView: RDEPUBReaderTopToolViewProviding =
context.controller?.dependencies.makeTopToolView?() ?? RDEPUBReaderTopToolView()
toolView.onBack = { [weak self] in
self?.handleBackAction()
}
@@ -31,8 +32,9 @@ final class RDEPUBReaderChromeCoordinator: NSObject, UIAdaptivePresentationContr
return toolView
}
func makeBottomToolView() -> RDEPUBReaderBottomToolView {
let toolView = RDEPUBReaderBottomToolView()
func makeBottomToolView() -> RDEPUBReaderBottomToolViewProviding {
let toolView: RDEPUBReaderBottomToolViewProviding =
context.controller?.dependencies.makeBottomToolView?() ?? RDEPUBReaderBottomToolView()
toolView.onShowTableOfContents = { [weak self] in
self?.presentTableOfContents()
}
@@ -35,13 +35,21 @@ public struct RDEPUBReaderDependencies {
public var makeTextRenderer: (RDEPUBTextRenderingEngine) -> RDEPUBTextRenderer
/// nil 使 `RDEPUBReaderTopToolView`
public var makeTopToolView: (() -> RDEPUBReaderTopToolViewProviding)?
/// nil 使 `RDEPUBReaderBottomToolView`
public var makeBottomToolView: (() -> RDEPUBReaderBottomToolViewProviding)?
public init(
environment: any RDEPUBReaderDisplayEnvironment,
makeParser: @escaping () -> RDEPUBParser,
makePaginator: @escaping () -> RDEPUBPaginator,
makeTextBookBuilder: @escaping (RDEPUBTextRenderer, RDEPUBTextBookCache?, RDEPUBTextLayoutConfig) -> RDEPUBTextBookBuilder,
makePlainTextBookBuilder: @escaping (RDEPUBTextRenderer, RDEPUBTextLayoutConfig) -> RDPlainTextBookBuilder,
makeTextRenderer: @escaping (RDEPUBTextRenderingEngine) -> RDEPUBTextRenderer
makeTextRenderer: @escaping (RDEPUBTextRenderingEngine) -> RDEPUBTextRenderer,
makeTopToolView: (() -> RDEPUBReaderTopToolViewProviding)? = nil,
makeBottomToolView: (() -> RDEPUBReaderBottomToolViewProviding)? = nil
) {
self.environment = environment
self.makeParser = makeParser
@@ -49,6 +57,22 @@ public struct RDEPUBReaderDependencies {
self.makeTextBookBuilder = makeTextBookBuilder
self.makePlainTextBookBuilder = makePlainTextBookBuilder
self.makeTextRenderer = makeTextRenderer
self.makeTopToolView = makeTopToolView
self.makeBottomToolView = makeBottomToolView
}
/// provider
/// 宿 `RDEPUBResourceDataProvider` EPUB
/// - Parameter resourceDataProvider: nil `.live`
public static func live(resourceDataProvider: RDEPUBResourceDataProvider?) -> RDEPUBReaderDependencies {
var dependencies = RDEPUBReaderDependencies.live
guard let resourceDataProvider else { return dependencies }
dependencies.makeParser = {
let parser = RDEPUBParser()
parser.resourceDataProvider = resourceDataProvider
return parser
}
return dependencies
}
public static var live: RDEPUBReaderDependencies {
@@ -80,6 +80,7 @@ final class RDEPUBReaderLoadCoordinator {
controller.applyReaderViewConfiguration()
context.updateReaderChrome()
controller.delegate?.epubReader(controller, didOpen: publication)
controller.applyOrientationLockIfNeeded()
context.runtime?.paginatePublication(restoreLocation: restoreLocation)
}
}
@@ -114,9 +114,12 @@ final class RDEPUBReaderPaginationCoordinator {
context.runtime?.viewportMonitor.processPendingChangeAfterPagination()
}
func repaginatePreservingCurrentLocation() {
func repaginatePreservingCurrentLocation(
preferredRestoreLocation: RDEPUBLocation? = nil
) {
guard context.publication != nil else { return }
let restoreLocation = context.runtime?.viewportMonitor.consumePendingPresentationRestoreLocation()
let restoreLocation = preferredRestoreLocation
?? context.runtime?.viewportMonitor.consumePendingPresentationRestoreLocation()
?? context.currentVisibleLocation()
?? context.persistenceLocation()
paginatePublication(restoreLocation: restoreLocation)
@@ -69,6 +69,15 @@ final class RDEPUBReaderRuntime {
private var pendingSettingsPreviewWorkItem: DispatchWorkItem?
/// The location before the settings preview replaces the full book page map
/// with the current chapter's temporary map. The temporary map rebases that
/// chapter at page zero, so it must never be used as the final restore source.
private var settingsRestoreLocation: RDEPUBLocation?
/// A settings session must keep one immutable text offset. Re-capturing the
/// start of each preview page makes repeated font-size changes drift backward.
private var settingsPreviewAnchor: SettingsPreviewAnchor?
private let settingsPreviewDebounceDelay: TimeInterval = 0.2
private struct SettingsPreviewAnchor {
@@ -84,11 +93,11 @@ final class RDEPUBReaderRuntime {
self.context = context
}
func makeTopToolView() -> RDEPUBReaderTopToolView {
func makeTopToolView() -> RDEPUBReaderTopToolViewProviding {
chromeCoordinator.makeTopToolView()
}
func makeBottomToolView() -> RDEPUBReaderBottomToolView {
func makeBottomToolView() -> RDEPUBReaderBottomToolViewProviding {
chromeCoordinator.makeBottomToolView()
}
@@ -379,7 +388,8 @@ final class RDEPUBReaderRuntime {
return
}
self.pendingSettingsPreviewWorkItem = nil
let previewAnchor = self.captureSettingsPreviewAnchor()
let previewAnchor = self.settingsPreviewAnchor
?? self.captureSettingsPreviewAnchor()
self.chapterRuntimeStore.invalidateAllForSettingsChange()
self.repaginateCurrentChapterOnly(
previewGeneration: previewGeneration,
@@ -504,6 +514,13 @@ final class RDEPUBReaderRuntime {
pendingSettingsPreviewWorkItem = nil
isSettingsPanelOpen = true
needsFullRepaginationAfterSettingsClose = false
let fallbackLocation = locationCoordinator.currentVisibleLocation()
?? context.persistenceLocation()
settingsPreviewAnchor = captureSettingsPreviewAnchor()
settingsRestoreLocation = exactSettingsRestoreLocation(
anchor: settingsPreviewAnchor,
fallbackLocation: fallbackLocation
)
settingsPreviewGeneration += 1
}
@@ -515,8 +532,35 @@ final class RDEPUBReaderRuntime {
if needsFullRepaginationAfterSettingsClose {
needsFullRepaginationAfterSettingsClose = false
paginationCoordinator.repaginatePreservingCurrentLocation()
paginationCoordinator.repaginatePreservingCurrentLocation(
preferredRestoreLocation: settingsRestoreLocation
)
}
settingsRestoreLocation = nil
settingsPreviewAnchor = nil
}
private func exactSettingsRestoreLocation(
anchor: SettingsPreviewAnchor?,
fallbackLocation: RDEPUBLocation?
) -> RDEPUBLocation? {
guard let anchor else { return fallbackLocation }
let textAnchor = RDEPUBTextAnchor(
fileIndex: anchor.spineIndex,
row: 0,
column: 0,
chapterOffset: anchor.offset,
fragmentID: fallbackLocation?.fragment
)
return RDEPUBLocation(
bookIdentifier: fallbackLocation?.bookIdentifier ?? context.currentBookIdentifier,
href: anchor.href,
progression: fallbackLocation?.progression ?? 0,
lastProgression: fallbackLocation?.lastProgression,
fragment: fallbackLocation?.fragment,
rangeAnchor: RDEPUBTextRangeAnchor(start: textAnchor, end: textAnchor)
)
}
func refreshVisibleContentPreservingLocation() {