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:
co-authored by
Claude Fable 5
parent
83dfa40299
commit
d5a7755702
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
+5
-2
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user