- 新增 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>
216 lines
7.9 KiB
Swift
216 lines
7.9 KiB
Swift
import UIKit
|
||
|
||
public enum RDEPUBTextRenderingEngine: Equatable {
|
||
|
||
case dtCoreText
|
||
}
|
||
|
||
public enum RDEPUBReaderFontChoice: String, Codable, CaseIterable, Equatable {
|
||
|
||
case system
|
||
|
||
case serif
|
||
|
||
case rounded
|
||
|
||
case monospaced
|
||
|
||
public var displayName: String {
|
||
switch self {
|
||
case .system:
|
||
return "系统"
|
||
case .serif:
|
||
return "宋体"
|
||
case .rounded:
|
||
return "圆体"
|
||
case .monospaced:
|
||
return "等宽"
|
||
}
|
||
}
|
||
|
||
public func font(ofSize size: CGFloat) -> UIFont {
|
||
switch self {
|
||
case .system:
|
||
return UIFont.systemFont(ofSize: size)
|
||
case .serif:
|
||
let descriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .body)
|
||
.withDesign(.serif) ?? UIFontDescriptor.preferredFontDescriptor(withTextStyle: .body)
|
||
return UIFont(descriptor: descriptor, size: size)
|
||
case .rounded:
|
||
let descriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .body)
|
||
.withDesign(.rounded) ?? UIFontDescriptor.preferredFontDescriptor(withTextStyle: .body)
|
||
return UIFont(descriptor: descriptor, size: size)
|
||
case .monospaced:
|
||
return UIFont.monospacedSystemFont(ofSize: size, weight: .regular)
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 试读策略:开启后,阅读器在最后一个可读页之后追加一页“试读墙”(UI 由宿主经 delegate 提供)。
|
||
public struct RDEPUBReaderTrialPolicy: Equatable {
|
||
|
||
/// 可读章节数(可读 spine 为 0..<readableChapterCount)。
|
||
/// nil = 全书可读,试读墙追加在全书末尾(对应 readoor 的独立试读 epub:整本都是试读章节)。
|
||
/// 指定数值时超出范围的章节页不再展示(对应“整本 epub + 按章限制”形态,仅 bookPageMap 路径生效)。
|
||
public var readableChapterCount: Int?
|
||
|
||
public init(readableChapterCount: Int? = nil) {
|
||
self.readableChapterCount = readableChapterCount
|
||
}
|
||
}
|
||
|
||
public struct RDEPUBReaderConfiguration: Equatable {
|
||
|
||
public var fontSize: CGFloat
|
||
|
||
public var lineHeightMultiple: CGFloat
|
||
|
||
public var fontChoice: RDEPUBReaderFontChoice
|
||
|
||
public var numberOfColumns: Int
|
||
|
||
public var columnGap: CGFloat
|
||
|
||
public var displayType: RDReaderView.DisplayType
|
||
|
||
public var landscapeDualPageEnabled: Bool
|
||
|
||
public var showsTableOfContents: Bool
|
||
|
||
public var allowsHighlights: Bool
|
||
|
||
public var showsSettingsPanel: Bool
|
||
|
||
public var reflowableContentInsets: UIEdgeInsets
|
||
|
||
public var fixedContentInset: UIEdgeInsets
|
||
|
||
public var theme: RDEPUBReaderTheme
|
||
|
||
public var darkImageAdjustmentEnabled: Bool
|
||
|
||
public var darkImageBlendRatio: CGFloat
|
||
|
||
public var fixedLayoutFit: RDEPUBFixedLayoutFit
|
||
|
||
public var fixedLayoutSpreadMode: RDEPUBFixedLayoutSpreadMode
|
||
|
||
public var textRenderingEngine: RDEPUBTextRenderingEngine
|
||
|
||
public var onDemandChapterWindowSize: Int
|
||
|
||
public var metadataParsingConcurrency: Int
|
||
|
||
public var jumpSessionPolicy: RDEPUBJumpSessionPolicy
|
||
|
||
public var allowedExternalURLSchemes: Set<String>
|
||
|
||
public var requiresExternalLinkConfirmation: Bool
|
||
|
||
public var allowsInspectableWebViews: Bool
|
||
|
||
public var enablesVerboseWebViewLogging: Bool
|
||
|
||
/// 试读策略;nil = 非试读(全书可读、无试读墙)。
|
||
public var trialPolicy: RDEPUBReaderTrialPolicy?
|
||
|
||
public init(
|
||
fontSize: CGFloat = 15,
|
||
lineHeightMultiple: CGFloat = 1.6,
|
||
fontChoice: RDEPUBReaderFontChoice = .system,
|
||
numberOfColumns: Int = 1,
|
||
columnGap: CGFloat = 20,
|
||
displayType: RDReaderView.DisplayType = .pageCurl,
|
||
landscapeDualPageEnabled: Bool = true,
|
||
showsTableOfContents: Bool = true,
|
||
allowsHighlights: Bool = true,
|
||
showsSettingsPanel: Bool = true,
|
||
reflowableContentInsets: UIEdgeInsets = RDEPUBSafeArea.defaultReflowableContentInsets(),
|
||
fixedContentInset: UIEdgeInsets = .zero,
|
||
theme: RDEPUBReaderTheme = .light,
|
||
darkImageAdjustmentEnabled: Bool = true,
|
||
darkImageBlendRatio: CGFloat = 0.15,
|
||
fixedLayoutFit: RDEPUBFixedLayoutFit = .page,
|
||
fixedLayoutSpreadMode: RDEPUBFixedLayoutSpreadMode = .automatic,
|
||
textRenderingEngine: RDEPUBTextRenderingEngine = .dtCoreText,
|
||
onDemandChapterWindowSize: Int = 3,
|
||
metadataParsingConcurrency: Int = ProcessInfo.processInfo.activeProcessorCount,
|
||
jumpSessionPolicy: RDEPUBJumpSessionPolicy = .default,
|
||
allowedExternalURLSchemes: Set<String> = ["https"],
|
||
requiresExternalLinkConfirmation: Bool = true,
|
||
allowsInspectableWebViews: Bool = false,
|
||
enablesVerboseWebViewLogging: Bool = false,
|
||
trialPolicy: RDEPUBReaderTrialPolicy? = nil
|
||
) {
|
||
self.fontSize = fontSize
|
||
self.lineHeightMultiple = lineHeightMultiple
|
||
self.fontChoice = fontChoice
|
||
self.numberOfColumns = max(1, numberOfColumns)
|
||
self.columnGap = max(0, columnGap)
|
||
self.displayType = displayType
|
||
self.landscapeDualPageEnabled = landscapeDualPageEnabled
|
||
self.showsTableOfContents = showsTableOfContents
|
||
self.allowsHighlights = allowsHighlights
|
||
self.showsSettingsPanel = showsSettingsPanel
|
||
self.reflowableContentInsets = reflowableContentInsets
|
||
self.fixedContentInset = fixedContentInset
|
||
self.theme = theme
|
||
self.darkImageAdjustmentEnabled = darkImageAdjustmentEnabled
|
||
self.darkImageBlendRatio = max(0, min(0.35, darkImageBlendRatio))
|
||
self.fixedLayoutFit = fixedLayoutFit
|
||
self.fixedLayoutSpreadMode = fixedLayoutSpreadMode
|
||
self.textRenderingEngine = textRenderingEngine
|
||
self.onDemandChapterWindowSize = Self.normalizedChapterWindowSize(onDemandChapterWindowSize)
|
||
self.metadataParsingConcurrency = max(1, metadataParsingConcurrency)
|
||
self.jumpSessionPolicy = jumpSessionPolicy
|
||
self.allowedExternalURLSchemes = allowedExternalURLSchemes
|
||
self.requiresExternalLinkConfirmation = requiresExternalLinkConfirmation
|
||
self.allowsInspectableWebViews = allowsInspectableWebViews
|
||
self.enablesVerboseWebViewLogging = enablesVerboseWebViewLogging
|
||
self.trialPolicy = trialPolicy
|
||
}
|
||
|
||
public static let `default` = RDEPUBReaderConfiguration()
|
||
}
|
||
|
||
extension RDEPUBReaderConfiguration {
|
||
|
||
static func normalizedChapterWindowSize(_ size: Int) -> Int {
|
||
let clamped = max(3, min(15, size))
|
||
return clamped % 2 == 0 ? clamped + 1 : clamped
|
||
}
|
||
|
||
var chapterWindowRadius: Int {
|
||
onDemandChapterWindowSize / 2
|
||
}
|
||
}
|
||
|
||
extension RDEPUBReaderConfiguration {
|
||
|
||
func makePreferences(safeAreaInsets: UIEdgeInsets = .zero) -> RDEPUBPreferences {
|
||
// Use the larger of reflowableContentInsets and safeAreaInsets for each edge
|
||
// to prevent content from being hidden under Dynamic Island / home indicator.
|
||
// Falls back to the key-window safe area when the caller has no laid-out view.
|
||
let resolvedSafeAreaInsets = RDEPUBSafeArea.resolve(safeAreaInsets)
|
||
let safeInsets = UIEdgeInsets(
|
||
top: max(reflowableContentInsets.top, resolvedSafeAreaInsets.top),
|
||
left: max(reflowableContentInsets.left, resolvedSafeAreaInsets.left),
|
||
bottom: max(reflowableContentInsets.bottom, resolvedSafeAreaInsets.bottom),
|
||
right: max(reflowableContentInsets.right, resolvedSafeAreaInsets.right)
|
||
)
|
||
return RDEPUBPreferences(
|
||
fontSize: fontSize,
|
||
lineHeightMultiple: lineHeightMultiple,
|
||
reflowableContentInsets: safeInsets,
|
||
fixedContentInset: fixedContentInset,
|
||
numberOfColumns: numberOfColumns,
|
||
columnGap: columnGap,
|
||
themeBackgroundColor: theme.themeBackgroundColorCSS,
|
||
themeTextColor: theme.themeTextColorCSS,
|
||
fixedBackgroundColor: theme.themeBackgroundColorCSS,
|
||
fixedLayoutFit: fixedLayoutFit,
|
||
fixedLayoutSpreadMode: fixedLayoutSpreadMode
|
||
)
|
||
}
|
||
}
|