refactor: 添加中文注释 + 优化模块结构
- 给全部 78 个 Swift 源文件添加详细的中文注释(文件级、类级、方法级) - 删除 LegacyRDReaderController/ 死代码目录(16 文件 4592 行) - 根目录翻页容器文件移入 ReaderView/ 目录 - Resources/ 移入 EPUBCore/Resources/(与使用者归属一致) - RDEPUBTextIndexTable.swift 移入 EPUBTextRendering/(消除反向依赖) - RDURLReaderController.swift 移入 EPUBUI/(入口控制器归入 UI 层) - 更新 podspec 资源路径
This commit is contained in:
@@ -4,6 +4,11 @@ import UIKit
|
||||
import DTCoreText
|
||||
#endif
|
||||
|
||||
// MARK: - 页面交互控制器
|
||||
|
||||
/// 页面交互控制器,负责文本层面的坐标映射与选择逻辑
|
||||
/// 在 DTCoreText 排版结果和用户交互之间充当桥梁
|
||||
/// 提供字符索引查找、选择范围计算、选择矩形计算等功能
|
||||
final class RDEPUBPageInteractionController {
|
||||
|
||||
var snapshot: RDEPUBPageLayoutSnapshot?
|
||||
|
||||
@@ -4,29 +4,56 @@ import UIKit
|
||||
import DTCoreText
|
||||
#endif
|
||||
|
||||
// MARK: - 页面排版快照数据结构
|
||||
|
||||
/// 文本行信息
|
||||
/// 描述 DTCoreText 排版后的一行文本的位置、范围和基线信息
|
||||
struct RDEPUBPageLine {
|
||||
/// 行内文本在 chapterContent 中的绝对字符范围
|
||||
let stringRange: NSRange
|
||||
/// 行的框架(相对于排版区域)
|
||||
let frame: CGRect
|
||||
/// 行的基线原点
|
||||
let baselineOrigin: CGPoint
|
||||
/// 基线以上的高度(升部)
|
||||
let ascent: CGFloat
|
||||
/// 基线以下的高度(降部)
|
||||
let descent: CGFloat
|
||||
/// 行间距
|
||||
let leading: CGFloat
|
||||
}
|
||||
|
||||
/// 文本 Run 信息
|
||||
/// 描述一行内某个连续绘制单元(如普通文字或附件)
|
||||
struct RDEPUBPageRun {
|
||||
/// Run 内文本的绝对字符范围
|
||||
let stringRange: NSRange
|
||||
/// Run 的绘制框架
|
||||
let frame: CGRect
|
||||
/// 是否为附件(图片等)
|
||||
let isAttachment: Bool
|
||||
}
|
||||
|
||||
/// 附件信息
|
||||
/// 描述嵌入在文本中的图片或其他附件
|
||||
struct RDEPUBPageAttachment {
|
||||
/// 附件在文本中的绝对字符范围
|
||||
let stringRange: NSRange
|
||||
/// 附件的显示框架
|
||||
let frame: CGRect
|
||||
/// 附件的建议显示尺寸
|
||||
let displaySize: CGSize
|
||||
/// 附件的布局方式(行内/浮动等)
|
||||
let placement: RDEPUBTextAttachmentPlacement?
|
||||
/// 附件类型(封面图/普通图片等)
|
||||
let kind: RDEPUBTextAttachmentKind?
|
||||
}
|
||||
|
||||
// MARK: - 页面排版快照
|
||||
|
||||
/// 页面排版快照
|
||||
/// 封装 DTCoreText 的排版结果,提供高效的文本位置查询能力
|
||||
/// 用于支持文本选择、高亮渲染和搜索结果定位
|
||||
struct RDEPUBPageLayoutSnapshot {
|
||||
let page: RDEPUBTextPage
|
||||
let lines: [RDEPUBPageLine]
|
||||
|
||||
@@ -1,12 +1,27 @@
|
||||
import UIKit
|
||||
|
||||
// MARK: - 底部工具栏
|
||||
|
||||
/// 阅读器底部工具栏
|
||||
/// 提供目录、书签、高亮管理、新建标注和设置五个功能入口
|
||||
/// 按钮可见性可根据 configuration 动态调整
|
||||
final class RDEPUBReaderBottomToolView: RDEPUBReaderToolView {
|
||||
// MARK: 回调闭包
|
||||
|
||||
/// 点击目录按钮回调
|
||||
var onShowTableOfContents: (() -> Void)?
|
||||
/// 点击书签列表按钮回调
|
||||
var onShowBookmarks: (() -> Void)?
|
||||
/// 点击高亮列表按钮回调
|
||||
var onShowHighlights: (() -> Void)?
|
||||
/// 点击新建标注按钮回调
|
||||
var onAddHighlight: (() -> Void)?
|
||||
/// 点击设置按钮回调
|
||||
var onShowSettings: (() -> Void)?
|
||||
|
||||
// MARK: UI 组件
|
||||
|
||||
/// 水平等分布局的按钮容器
|
||||
private let stackView: UIStackView = {
|
||||
let view = UIStackView()
|
||||
view.axis = .horizontal
|
||||
@@ -16,10 +31,15 @@ final class RDEPUBReaderBottomToolView: RDEPUBReaderToolView {
|
||||
return view
|
||||
}()
|
||||
|
||||
/// 目录按钮
|
||||
private let chapterButton = RDEPUBReaderTintButton(type: .system)
|
||||
/// 书签列表按钮
|
||||
private let bookmarksButton = RDEPUBReaderTintButton(type: .system)
|
||||
/// 高亮列表按钮
|
||||
private let highlightsButton = RDEPUBReaderTintButton(type: .system)
|
||||
/// 新建标注按钮
|
||||
private let addHighlightButton = RDEPUBReaderTintButton(type: .system)
|
||||
/// 设置按钮
|
||||
private let settingsButton = RDEPUBReaderTintButton(type: .system)
|
||||
|
||||
override init(frame: CGRect) {
|
||||
@@ -78,6 +98,9 @@ final class RDEPUBReaderBottomToolView: RDEPUBReaderToolView {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: 按钮状态控制
|
||||
|
||||
/// 设置新建标注按钮是否可用(有选中文本时可用)
|
||||
func setAddHighlightEnabled(_ isEnabled: Bool) {
|
||||
addHighlightButton.isEnabled = isEnabled
|
||||
addHighlightButton.alpha = isEnabled ? 1 : 0.45
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import UIKit
|
||||
|
||||
// MARK: - 目录面板
|
||||
|
||||
/// EPUB 阅读器的目录面板控制器
|
||||
/// 以列表形式展示书籍目录,支持多级缩进,当前所在章节高亮显示
|
||||
final class RDEPUBReaderChapterListController: UITableViewController {
|
||||
/// 用户选择目录项时的回调
|
||||
var onSelectItem: ((RDEPUBReaderTableOfContentsItem) -> Void)?
|
||||
|
||||
private let items: [RDEPUBReaderTableOfContentsItem]
|
||||
|
||||
@@ -1,24 +1,75 @@
|
||||
import UIKit
|
||||
|
||||
// MARK: - 文本渲染引擎
|
||||
|
||||
/// EPUB 文本渲染引擎枚举
|
||||
/// 定义了当前支持的文本渲染方式,未来可扩展为多引擎选择
|
||||
public enum RDEPUBTextRenderingEngine: Equatable {
|
||||
/// 基于 DTCoreText 的渲染引擎,将 HTML/CSS 转换为 NSAttributedString
|
||||
case dtCoreText
|
||||
}
|
||||
|
||||
// MARK: - 阅读器配置
|
||||
|
||||
/// EPUB 阅读器的完整配置结构体
|
||||
/// 作为 EPUBUI 层的入口配置,控制阅读器的外观、行为和功能开关
|
||||
/// 调用方在创建 RDEPUBReaderController 前可自定义此配置
|
||||
public struct RDEPUBReaderConfiguration: Equatable {
|
||||
// MARK: 阅读排版参数
|
||||
|
||||
/// 正文字号(单位:pt),默认 15
|
||||
public var fontSize: CGFloat
|
||||
/// 行距倍数,默认 1.6(即行距为字号的 1.6 倍)
|
||||
public var lineHeightMultiple: CGFloat
|
||||
/// 翻页显示模式:.pageCurl(仿真翻页)或 .scroll(滚动阅读)
|
||||
public var displayType: RDReaderView.DisplayType
|
||||
/// 横屏时是否启用双页显示,默认开启
|
||||
public var landscapeDualPageEnabled: Bool
|
||||
|
||||
// MARK: UI 功能开关
|
||||
|
||||
/// 是否显示目录入口,默认开启
|
||||
public var showsTableOfContents: Bool
|
||||
/// 是否允许文本高亮功能,默认开启
|
||||
public var allowsHighlights: Bool
|
||||
/// 是否显示设置面板入口,默认开启
|
||||
public var showsSettingsPanel: Bool
|
||||
|
||||
// MARK: 内容内边距
|
||||
|
||||
/// 流式排版(reflowable)内容的内边距,上下 40pt、左右 16pt
|
||||
public var reflowableContentInsets: UIEdgeInsets
|
||||
/// 固定布局(fixed layout)内容的内边距,默认为零
|
||||
public var fixedContentInset: UIEdgeInsets
|
||||
|
||||
// MARK: 主题与布局
|
||||
|
||||
/// 当前阅读主题(浅色/深色/护眼等),默认 .light
|
||||
public var theme: RDEPUBReaderTheme
|
||||
/// 固定布局的适配模式:按页适配或按宽度适配
|
||||
public var fixedLayoutFit: RDEPUBFixedLayoutFit
|
||||
/// 固定布局的跨页模式:自动/单页/双页
|
||||
public var fixedLayoutSpreadMode: RDEPUBFixedLayoutSpreadMode
|
||||
/// 文本渲染引擎,默认使用 DTCoreText
|
||||
public var textRenderingEngine: RDEPUBTextRenderingEngine
|
||||
|
||||
// MARK: 初始化
|
||||
|
||||
/// 创建阅读器配置,所有参数均提供合理的默认值
|
||||
/// - Parameters:
|
||||
/// - fontSize: 正文字号,默认 15pt
|
||||
/// - lineHeightMultiple: 行距倍数,默认 1.6
|
||||
/// - displayType: 翻页模式,默认 .pageCurl
|
||||
/// - landscapeDualPageEnabled: 横屏双页,默认 true
|
||||
/// - showsTableOfContents: 显示目录入口,默认 true
|
||||
/// - allowsHighlights: 允许高亮,默认 true
|
||||
/// - showsSettingsPanel: 显示设置面板,默认 true
|
||||
/// - reflowableContentInsets: 流式排版内边距
|
||||
/// - fixedContentInset: 固定布局内边距
|
||||
/// - theme: 阅读主题,默认 .light
|
||||
/// - fixedLayoutFit: 固定布局适配模式
|
||||
/// - fixedLayoutSpreadMode: 固定布局跨页模式
|
||||
/// - textRenderingEngine: 文本渲染引擎
|
||||
public init(
|
||||
fontSize: CGFloat = 15,
|
||||
lineHeightMultiple: CGFloat = 1.6,
|
||||
@@ -49,10 +100,15 @@ public struct RDEPUBReaderConfiguration: Equatable {
|
||||
self.textRenderingEngine = textRenderingEngine
|
||||
}
|
||||
|
||||
/// 默认配置实例,使用所有参数的默认值
|
||||
public static let `default` = RDEPUBReaderConfiguration()
|
||||
}
|
||||
|
||||
// MARK: - 配置转换
|
||||
|
||||
extension RDEPUBReaderConfiguration {
|
||||
/// 将用户可见的配置转换为底层排版引擎所需的 RDEPUBPreferences
|
||||
/// - Returns: 排版偏好设置实例,供 Core 层的渲染管道使用
|
||||
func makePreferences() -> RDEPUBPreferences {
|
||||
RDEPUBPreferences(
|
||||
fontSize: fontSize,
|
||||
|
||||
@@ -1,6 +1,29 @@
|
||||
import UIKit
|
||||
|
||||
// MARK: - 文件说明
|
||||
|
||||
/// EPUB 阅读器主控制器
|
||||
/// EPUBUI 层的核心入口,提供开箱即用的完整阅读器体验
|
||||
/// 调用方只需传入 EPUB 文件 URL 或已构建的 TextBook,即可呈现完整的阅读界面
|
||||
///
|
||||
/// **架构位置:** EPUBUI(最顶层)→ Core → Parser → Foundation
|
||||
///
|
||||
/// **核心职责:**
|
||||
/// - EPUB 解析与分页(支持流式排版和固定布局)
|
||||
/// - 阅读位置的持久化与恢复
|
||||
/// - 高亮、书签的增删改查
|
||||
/// - 工具栏、目录、设置等 UI 面板的协调管理
|
||||
/// - 搜索功能(全文搜索、前后跳转)
|
||||
///
|
||||
/// **打开流程:** init(epubURL:) → viewDidLoad → RDEPUBParser.parse → paginatePublication → readerView.reloadData → restoreLocation
|
||||
|
||||
// MARK: - 阅读器主控制器
|
||||
|
||||
public final class RDEPUBReaderController: UIViewController {
|
||||
// MARK: - 内部辅助类型
|
||||
|
||||
/// 视口签名结构体,用于检测视口尺寸或安全区是否发生显著变化
|
||||
/// 当变化超过阈值时触发重新分页
|
||||
private struct RDEPUBViewportSignature: Equatable {
|
||||
let width: CGFloat
|
||||
let height: CGFloat
|
||||
@@ -19,6 +42,7 @@ public final class RDEPUBReaderController: UIViewController {
|
||||
}
|
||||
}
|
||||
|
||||
/// 视口变化的原因枚举,用于决定延迟处理的策略
|
||||
private enum RDEPUBViewportChangeReason {
|
||||
case viewLayout
|
||||
case orientationTransition
|
||||
@@ -27,8 +51,12 @@ public final class RDEPUBReaderController: UIViewController {
|
||||
|
||||
private typealias NativeTextSnapshot = (pages: [EPUBPage], chapters: [EPUBChapterInfo])
|
||||
|
||||
// MARK: - 公开属性
|
||||
|
||||
/// 阅读器委托,用于接收阅读状态变更通知
|
||||
public weak var delegate: RDEPUBReaderDelegate?
|
||||
|
||||
/// 阅读器配置,修改后自动判断是否需要重新分页或刷新内容
|
||||
public var configuration: RDEPUBReaderConfiguration {
|
||||
didSet {
|
||||
persistReaderSettingsIfNeeded()
|
||||
@@ -54,38 +82,49 @@ public final class RDEPUBReaderController: UIViewController {
|
||||
}
|
||||
}
|
||||
|
||||
/// 当前阅读位置
|
||||
public var currentLocation: RDEPUBLocation? {
|
||||
currentVisibleLocation()
|
||||
}
|
||||
|
||||
/// 当前页码(从 1 开始),nil 表示尚未加载
|
||||
public var currentPageNumber: Int? {
|
||||
guard readerView.currentPage >= 0 else { return nil }
|
||||
return readerView.currentPage + 1
|
||||
}
|
||||
|
||||
/// 当前用户选中的文本(只读),用于创建高亮/批注
|
||||
public private(set) var currentSelection: RDEPUBSelection?
|
||||
|
||||
/// 所有高亮标注
|
||||
public var highlights: [RDEPUBHighlight] {
|
||||
activeHighlights
|
||||
}
|
||||
|
||||
/// 所有书签
|
||||
public var bookmarks: [RDEPUBBookmark] {
|
||||
activeBookmarks
|
||||
}
|
||||
|
||||
/// 原始目录树结构
|
||||
public var tableOfContents: [EPUBTableOfContentsItem] {
|
||||
publication?.tableOfContents ?? []
|
||||
}
|
||||
|
||||
/// 展平后的目录列表(用于目录面板展示,每个条目附带页码)
|
||||
public var flattenedTableOfContents: [RDEPUBReaderTableOfContentsItem] {
|
||||
guard let publication else { return [] }
|
||||
return flattenedTableOfContentsItems(from: publication.tableOfContents)
|
||||
}
|
||||
|
||||
/// 当前阅读位置所在的目录项
|
||||
public var currentTableOfContentsItem: RDEPUBReaderTableOfContentsItem? {
|
||||
resolvedCurrentTableOfContentsItem()
|
||||
}
|
||||
|
||||
// MARK: - 私有属性
|
||||
|
||||
/// EPUB 文件 URL
|
||||
private let epubURL: URL
|
||||
fileprivate let persistence: RDEPUBReaderPersistence?
|
||||
private let readerView = RDReaderView()
|
||||
@@ -131,6 +170,14 @@ public final class RDEPUBReaderController: UIViewController {
|
||||
/// 外部 TextBook 的原始文件 URL(用于重建 TextBook)
|
||||
private var textFileURL: URL?
|
||||
|
||||
// MARK: - 初始化
|
||||
|
||||
/// 使用 EPUB 文件 URL 初始化阅读器
|
||||
/// 会自动从持久化存储中恢复用户的阅读偏好设置
|
||||
/// - Parameters:
|
||||
/// - epubURL: EPUB 文件的本地 URL
|
||||
/// - configuration: 阅读器配置,默认使用 .default
|
||||
/// - persistence: 持久化策略,默认使用 UserDefaults
|
||||
public init(
|
||||
epubURL: URL,
|
||||
configuration: RDEPUBReaderConfiguration = .default,
|
||||
@@ -149,7 +196,15 @@ public final class RDEPUBReaderController: UIViewController {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
/// 使用已构建的 TextBook 初始化,跳过 EPUB 解析流程(用于 TXT 等纯文本文件)
|
||||
/// 使用已构建的 TextBook 初始化,跳过 EPUB 解析流程
|
||||
/// 适用于 TXT 等纯文本文件,调用方需自行构建 TextBook
|
||||
/// - Parameters:
|
||||
/// - textBook: 已分页的 TextBook 实例
|
||||
/// - bookIdentifier: 书籍唯一标识符,用于持久化
|
||||
/// - title: 书籍标题
|
||||
/// - textFileURL: 原始文本文件 URL,用于重建 TextBook
|
||||
/// - configuration: 阅读器配置
|
||||
/// - persistence: 持久化策略
|
||||
public convenience init(
|
||||
textBook: RDEPUBTextBook,
|
||||
bookIdentifier: String,
|
||||
@@ -244,6 +299,9 @@ public final class RDEPUBReaderController: UIViewController {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 公开 API
|
||||
|
||||
/// 重新加载书籍,重置所有状态并重新解析 EPUB
|
||||
public func reloadBook() {
|
||||
didStartInitialLoad = false
|
||||
parser = nil
|
||||
@@ -263,11 +321,18 @@ public final class RDEPUBReaderController: UIViewController {
|
||||
startInitialLoadIfNeeded()
|
||||
}
|
||||
|
||||
/// 跳转到指定阅读位置
|
||||
/// - Parameter location: 目标阅读位置
|
||||
public func go(to location: RDEPUBLocation) {
|
||||
guard publication != nil else { return }
|
||||
restoreReadingLocation(location)
|
||||
}
|
||||
|
||||
/// 跳转到指定页码
|
||||
/// - Parameters:
|
||||
/// - pageNumber: 目标页码(从 1 开始)
|
||||
/// - animated: 是否动画过渡
|
||||
/// - Returns: 是否跳转成功
|
||||
@discardableResult
|
||||
public func go(toPageNumber pageNumber: Int, animated: Bool = false) -> Bool {
|
||||
guard pageNumber > 0 else { return false }
|
||||
@@ -289,6 +354,7 @@ public final class RDEPUBReaderController: UIViewController {
|
||||
return true
|
||||
}
|
||||
|
||||
/// 清除当前文本选中状态
|
||||
public func clearSelection() {
|
||||
updateCurrentSelection(nil)
|
||||
}
|
||||
@@ -321,6 +387,12 @@ public final class RDEPUBReaderController: UIViewController {
|
||||
return parts.joined(separator: " · ")
|
||||
}
|
||||
|
||||
/// 添加高亮标注,从当前选中文本或指定选区创建
|
||||
/// - Parameters:
|
||||
/// - selection: 文本选区,默认使用 currentSelection
|
||||
/// - color: 高亮颜色(CSS 格式),默认黄色
|
||||
/// - note: 可选批注文字
|
||||
/// - Returns: 创建的高亮对象,重复时返回 nil
|
||||
@discardableResult
|
||||
public func addHighlight(
|
||||
from selection: RDEPUBSelection? = nil,
|
||||
@@ -1285,6 +1357,8 @@ public final class RDEPUBReaderController: UIViewController {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 书签管理
|
||||
|
||||
extension RDEPUBReaderController {
|
||||
@discardableResult
|
||||
public func addBookmark(note: String? = nil) -> RDEPUBBookmark? {
|
||||
@@ -1456,6 +1530,10 @@ extension RDEPUBReaderController {
|
||||
return trimmed.isEmpty ? nil : trimmed
|
||||
}
|
||||
|
||||
// MARK: - 搜索功能
|
||||
|
||||
/// 执行全文搜索,自动跳转到第一个匹配项
|
||||
/// - Parameter keyword: 搜索关键词,空字符串会清除搜索
|
||||
public func search(keyword: String) {
|
||||
let normalizedKeyword = keyword.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !normalizedKeyword.isEmpty else {
|
||||
@@ -1622,6 +1700,8 @@ extension RDEPUBReaderController {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - RDReaderView 数据源与代理
|
||||
|
||||
extension RDEPUBReaderController: RDReaderDataSource, RDReaderDelegate {
|
||||
public func pageCountOfReaderView(readerView: RDReaderView) -> Int {
|
||||
textBook?.pages.count ?? activePages.count
|
||||
@@ -1724,6 +1804,8 @@ extension RDEPUBReaderController: RDReaderDataSource, RDReaderDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Web 内容视图代理(EPUB 固定布局/Web 渲染路径)
|
||||
|
||||
extension RDEPUBReaderController: RDEPUBWebContentViewDelegate {
|
||||
func epubWebContentView(_ contentView: RDEPUBWebContentView, didUpdateLocation location: RDEPUBLocation, spineIndex: Int) {
|
||||
guard readerView.currentPage >= 0,
|
||||
@@ -1780,6 +1862,8 @@ extension RDEPUBReaderController: RDEPUBWebContentViewDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 文本内容视图代理(Native Text 渲染路径)
|
||||
|
||||
extension RDEPUBReaderController: RDEPUBTextContentViewDelegate {
|
||||
func textContentView(_ contentView: RDEPUBTextContentView, didChangeSelection selection: RDEPUBSelection?) {
|
||||
guard let selection else {
|
||||
@@ -1902,6 +1986,8 @@ extension RDEPUBReaderController: RDEPUBTextContentViewDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 手势识别器代理(用于 NavigationController 返回手势)
|
||||
|
||||
extension RDEPUBReaderController: UIGestureRecognizerDelegate {
|
||||
public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
true
|
||||
|
||||
@@ -1,20 +1,86 @@
|
||||
import UIKit
|
||||
|
||||
// MARK: - 阅读器委托协议
|
||||
|
||||
/// EPUB 阅读器的委托协议
|
||||
/// 用于向调用方通知阅读器的各种状态变更事件
|
||||
/// 所有方法均提供默认空实现,调用方可选择性实现感兴趣的方法
|
||||
public protocol RDEPUBReaderDelegate: AnyObject {
|
||||
/// 阅读器成功打开书籍时调用
|
||||
/// - Parameters:
|
||||
/// - reader: 阅读器控制器
|
||||
/// - publication: 解析后的出版物对象
|
||||
func epubReader(_ reader: UIViewController, didOpen publication: RDEPUBPublication)
|
||||
|
||||
/// 阅读位置更新时调用(翻页、跳转等)
|
||||
/// - Parameters:
|
||||
/// - reader: 阅读器控制器
|
||||
/// - location: 新的阅读位置
|
||||
func epubReader(_ reader: UIViewController, didUpdateLocation location: RDEPUBLocation)
|
||||
|
||||
/// 阅读器翻到书籍末尾时调用
|
||||
/// - Parameter reader: 阅读器控制器
|
||||
func epubReaderDidReachEnd(_ reader: UIViewController)
|
||||
|
||||
/// 用户选中文本发生变化时调用
|
||||
/// - Parameters:
|
||||
/// - reader: 阅读器控制器
|
||||
/// - selection: 当前选中文本,nil 表示取消选中
|
||||
func epubReader(_ reader: UIViewController, didChangeSelection selection: RDEPUBSelection?)
|
||||
|
||||
/// 高亮标注列表更新时调用
|
||||
/// - Parameters:
|
||||
/// - reader: 阅读器控制器
|
||||
/// - highlights: 当前所有高亮
|
||||
func epubReader(_ reader: UIViewController, didUpdateHighlights highlights: [RDEPUBHighlight])
|
||||
|
||||
/// 书签列表更新时调用
|
||||
/// - Parameters:
|
||||
/// - reader: 阅读器控制器
|
||||
/// - bookmarks: 当前所有书签
|
||||
func epubReader(_ reader: UIViewController, didUpdateBookmarks bookmarks: [RDEPUBBookmark])
|
||||
|
||||
/// 搜索结果更新时调用
|
||||
/// - Parameters:
|
||||
/// - reader: 阅读器控制器
|
||||
/// - result: 搜索结果摘要,nil 表示无搜索
|
||||
func epubReader(_ reader: UIViewController, didUpdateSearchResult result: RDEPUBSearchResult?)
|
||||
|
||||
/// 当前搜索匹配项变化时调用
|
||||
/// - Parameters:
|
||||
/// - reader: 阅读器控制器
|
||||
/// - match: 当前匹配项,nil 表示无匹配
|
||||
func epubReader(_ reader: UIViewController, didChangeCurrentSearchMatch match: RDEPUBSearchMatch?)
|
||||
|
||||
/// 当前所在目录项更新时调用
|
||||
/// - Parameters:
|
||||
/// - reader: 阅读器控制器
|
||||
/// - item: 当前目录项
|
||||
func epubReader(_ reader: UIViewController, didUpdateCurrentTableOfContentsItem item: RDEPUBReaderTableOfContentsItem?)
|
||||
|
||||
/// 用户点击外部链接时调用
|
||||
/// - Parameters:
|
||||
/// - reader: 阅读器控制器
|
||||
/// - url: 外部链接 URL
|
||||
func epubReader(_ reader: UIViewController, didActivateExternalLink url: URL)
|
||||
|
||||
/// 阅读器发生错误时调用
|
||||
/// - Parameters:
|
||||
/// - reader: 阅读器控制器
|
||||
/// - error: 错误对象
|
||||
func epubReader(_ reader: UIViewController, didFailWithError error: Error)
|
||||
|
||||
/// 允许调用方自定义顶部工具栏
|
||||
/// 在 viewDidLoad 时调用,可在工具栏上添加自定义按钮
|
||||
/// - Parameters:
|
||||
/// - reader: 阅读器控制器
|
||||
/// - topToolView: 顶部工具栏视图
|
||||
func epubReader(_ reader: UIViewController, configureTopToolView topToolView: RDEPUBReaderTopToolView)
|
||||
}
|
||||
|
||||
// MARK: - 默认空实现
|
||||
|
||||
/// 所有委托方法的默认空实现,调用方可只实现需要的方法
|
||||
public extension RDEPUBReaderDelegate {
|
||||
func epubReader(_ reader: UIViewController, didOpen publication: RDEPUBPublication) {}
|
||||
func epubReader(_ reader: UIViewController, didUpdateLocation location: RDEPUBLocation) {}
|
||||
|
||||
@@ -1,13 +1,24 @@
|
||||
import UIKit
|
||||
|
||||
// MARK: - 高亮管理面板
|
||||
|
||||
/// EPUB 阅读器的高亮标注管理控制器
|
||||
/// 以列表形式展示所有高亮和批注,支持筛选(全部/批注/高亮)、跳转到位置、编辑批注、删除
|
||||
final class RDEPUBReaderHighlightsViewController: UITableViewController {
|
||||
/// 用户选择跳转到某个高亮位置时的回调
|
||||
var onSelectHighlight: ((RDEPUBHighlight) -> Void)?
|
||||
/// 用户编辑高亮批注后的回调
|
||||
var onUpdateHighlight: ((RDEPUBHighlight) -> Void)?
|
||||
/// 用户删除高亮时的回调
|
||||
var onDeleteHighlight: ((RDEPUBHighlight) -> Void)?
|
||||
|
||||
/// 高亮列表(按创建时间降序排列)
|
||||
private var highlights: [RDEPUBHighlight]
|
||||
/// 当前主题配置
|
||||
private let theme: RDEPUBReaderTheme
|
||||
/// 提供章节标题的回调,用于显示高亮所在的章节名
|
||||
private let sectionTitleProvider: (RDEPUBHighlight) -> String?
|
||||
/// 筛选控件:全部、批注、高亮
|
||||
private let filterControl = UISegmentedControl(items: ["全部", "批注", "高亮"])
|
||||
|
||||
private var filteredHighlights: [RDEPUBHighlight] {
|
||||
@@ -205,8 +216,14 @@ final class RDEPUBReaderHighlightsViewController: UITableViewController {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 书签管理面板
|
||||
|
||||
/// EPUB 阅读器的书签管理控制器
|
||||
/// 以列表形式展示所有书签,支持跳转到书签位置和删除书签
|
||||
final class RDEPUBReaderBookmarksViewController: UITableViewController {
|
||||
/// 用户选择跳转到某个书签位置时的回调
|
||||
var onSelectBookmark: ((RDEPUBBookmark) -> Void)?
|
||||
/// 用户删除书签时的回调
|
||||
var onDeleteBookmark: ((RDEPUBBookmark) -> Void)?
|
||||
|
||||
private var bookmarks: [RDEPUBBookmark]
|
||||
|
||||
@@ -1,16 +1,44 @@
|
||||
import Foundation
|
||||
|
||||
// MARK: - 持久化协议
|
||||
|
||||
/// 阅读器持久化协议
|
||||
/// 定义了阅读位置、书签、高亮和用户设置的存储接口
|
||||
/// 调用方可实现此协议以对接自定义存储(如数据库、云端同步等)
|
||||
public protocol RDEPUBReaderPersistence: AnyObject {
|
||||
/// 加载指定书籍的上次阅读位置
|
||||
/// - Parameter bookIdentifier: 书籍唯一标识符
|
||||
/// - Returns: 保存的阅读位置,nil 表示首次打开
|
||||
func loadLocation(for bookIdentifier: String) -> RDEPUBLocation?
|
||||
|
||||
/// 保存指定书籍的当前阅读位置
|
||||
/// - Parameters:
|
||||
/// - location: 阅读位置
|
||||
/// - bookIdentifier: 书籍唯一标识符
|
||||
func saveLocation(_ location: RDEPUBLocation, for bookIdentifier: String)
|
||||
|
||||
/// 加载指定书籍的所有书签
|
||||
func loadBookmarks(for bookIdentifier: String) -> [RDEPUBBookmark]
|
||||
|
||||
/// 保存指定书籍的所有书签
|
||||
func saveBookmarks(_ bookmarks: [RDEPUBBookmark], for bookIdentifier: String)
|
||||
|
||||
/// 加载指定书籍的所有高亮标注
|
||||
func loadHighlights(for bookIdentifier: String) -> [RDEPUBHighlight]
|
||||
|
||||
/// 保存指定书籍的所有高亮标注
|
||||
func saveHighlights(_ highlights: [RDEPUBHighlight], for bookIdentifier: String)
|
||||
|
||||
/// 加载全局阅读器用户设置(字号、行距、主题等)
|
||||
func loadReaderSettings() -> RDEPUBReaderSettings?
|
||||
|
||||
/// 保存全局阅读器用户设置
|
||||
func saveReaderSettings(_ settings: RDEPUBReaderSettings)
|
||||
}
|
||||
|
||||
// MARK: - 默认实现
|
||||
|
||||
/// 协议的默认空实现,书签/高亮/设置为可选功能
|
||||
public extension RDEPUBReaderPersistence {
|
||||
func loadBookmarks(for bookIdentifier: String) -> [RDEPUBBookmark] {
|
||||
_ = bookIdentifier
|
||||
@@ -31,13 +59,30 @@ public extension RDEPUBReaderPersistence {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UserDefaults 持久化实现
|
||||
|
||||
/// 基于 UserDefaults 的持久化实现
|
||||
/// 数据以 JSON 格式存储,按书籍标识符分隔存储位置
|
||||
/// 适合轻量级场景,大量数据建议改用 SQLite 或文件系统
|
||||
public final class RDEPUBUserDefaultsPersistence: RDEPUBReaderPersistence {
|
||||
/// UserDefaults 实例
|
||||
private let defaults: UserDefaults
|
||||
/// 阅读位置的 key 前缀
|
||||
private let locationPrefix: String
|
||||
/// 书签列表的 key 前缀
|
||||
private let bookmarksPrefix: String
|
||||
/// 高亮列表的 key 前缀
|
||||
private let highlightsPrefix: String
|
||||
/// 用户设置的 key
|
||||
private let settingsKey: String
|
||||
|
||||
/// 初始化 UserDefaults 持久化策略
|
||||
/// - Parameters:
|
||||
/// - defaults: UserDefaults 实例,默认 .standard
|
||||
/// - locationPrefix: 阅读位置 key 前缀
|
||||
/// - bookmarksPrefix: 书签 key 前缀
|
||||
/// - highlightsPrefix: 高亮 key 前缀
|
||||
/// - settingsKey: 用户设置 key
|
||||
public init(
|
||||
defaults: UserDefaults = .standard,
|
||||
locationPrefix: String = "ssreader.epub.location.",
|
||||
|
||||
@@ -1,11 +1,20 @@
|
||||
import UIKit
|
||||
|
||||
// MARK: - 翻页显示模式
|
||||
|
||||
/// 阅读器翻页显示模式枚举
|
||||
/// 用于持久化存储用户的翻页偏好设置,与 RDReaderView.DisplayType 相互转换
|
||||
public enum RDEPUBReaderDisplayMode: String, Codable, Equatable {
|
||||
/// 仿真翻页模式(纸张翻转效果)
|
||||
case pageCurl
|
||||
/// 水平滚动模式
|
||||
case horizontalScroll
|
||||
/// 垂直滚动模式
|
||||
case verticalScroll
|
||||
/// 水平覆盖式滚动模式(类似电子杂志翻页效果)
|
||||
case horizontalCoverScroll
|
||||
|
||||
/// 从底层 DisplayType 转换为 UI 层的 DisplayMode
|
||||
init(displayType: RDReaderView.DisplayType) {
|
||||
switch displayType {
|
||||
case .pageCurl:
|
||||
@@ -17,6 +26,8 @@ public enum RDEPUBReaderDisplayMode: String, Codable, Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
/// 转换为底层 RDReaderView 所需的 DisplayType
|
||||
/// 注意:horizontalCoverScroll 会被映射为 horizontalScroll
|
||||
var displayType: RDReaderView.DisplayType {
|
||||
switch self {
|
||||
case .pageCurl:
|
||||
@@ -29,14 +40,25 @@ public enum RDEPUBReaderDisplayMode: String, Codable, Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 主题预设
|
||||
|
||||
/// 阅读器主题预设枚举
|
||||
/// 将 RDEPUBReaderTheme 映射为可序列化的枚举值,用于持久化存储和设置面板展示
|
||||
public enum RDEPUBReaderThemePreset: String, Codable, CaseIterable, Equatable {
|
||||
/// 浅色主题
|
||||
case light
|
||||
/// 黄色护眼主题
|
||||
case yellow
|
||||
/// 绿色护眼主题
|
||||
case green
|
||||
/// 粉色主题
|
||||
case pink
|
||||
/// 蓝色主题
|
||||
case blue
|
||||
/// 深色/夜间主题
|
||||
case dark
|
||||
|
||||
/// 将预设枚举转换为主题配置实例
|
||||
public var theme: RDEPUBReaderTheme {
|
||||
switch self {
|
||||
case .light:
|
||||
@@ -54,6 +76,8 @@ public enum RDEPUBReaderThemePreset: String, Codable, CaseIterable, Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
/// 从主题配置实例反向查找对应的预设枚举
|
||||
/// 自定义主题(非内置预设)会返回 nil
|
||||
public init?(theme: RDEPUBReaderTheme) {
|
||||
switch theme {
|
||||
case .light:
|
||||
@@ -74,13 +98,26 @@ public enum RDEPUBReaderThemePreset: String, Codable, CaseIterable, Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 阅读器用户设置
|
||||
|
||||
/// 阅读器的可持久化用户设置
|
||||
/// 用于保存用户在设置面板中调整的阅读偏好(字号、行距、翻页模式、主题等)
|
||||
/// 所有属性均为可选类型,只覆盖用户实际修改过的配置项
|
||||
public struct RDEPUBReaderSettings: Codable, Equatable {
|
||||
// MARK: 可调参数
|
||||
|
||||
/// 屏幕亮度(0.0 ~ 1.0),nil 表示使用系统亮度
|
||||
public var brightness: CGFloat?
|
||||
/// 用户选择的正文字号(pt),nil 表示使用默认值
|
||||
public var fontSize: CGFloat?
|
||||
/// 用户选择的行距倍数,nil 表示使用默认值
|
||||
public var lineHeightMultiple: CGFloat?
|
||||
/// 用户选择的翻页模式,nil 表示使用默认值
|
||||
public var displayMode: RDEPUBReaderDisplayMode?
|
||||
/// 用户选择的主题预设,nil 表示使用默认值
|
||||
public var themePreset: RDEPUBReaderThemePreset?
|
||||
|
||||
/// 初始化用户设置,所有参数默认为 nil
|
||||
public init(
|
||||
brightness: CGFloat? = nil,
|
||||
fontSize: CGFloat? = nil,
|
||||
@@ -95,6 +132,10 @@ public struct RDEPUBReaderSettings: Codable, Equatable {
|
||||
self.themePreset = themePreset
|
||||
}
|
||||
|
||||
/// 将用户设置覆盖到基础配置上,得到最终生效的阅读器配置
|
||||
/// nil 属性不会覆盖基础配置的对应值
|
||||
/// - Parameter configuration: 基础配置(通常来自 RDEPUBReaderConfiguration.default)
|
||||
/// - Returns: 合并后的完整配置
|
||||
public func applying(to configuration: RDEPUBReaderConfiguration) -> RDEPUBReaderConfiguration {
|
||||
var resolvedConfiguration = configuration
|
||||
|
||||
@@ -114,6 +155,12 @@ public struct RDEPUBReaderSettings: Codable, Equatable {
|
||||
return resolvedConfiguration
|
||||
}
|
||||
|
||||
/// 从当前配置和亮度值捕获一份完整的用户设置快照
|
||||
/// 亮度值会被限制在 0.0 ~ 1.0 范围内
|
||||
/// - Parameters:
|
||||
/// - configuration: 当前生效的阅读器配置
|
||||
/// - brightness: 当前屏幕亮度
|
||||
/// - Returns: 完整的用户设置实例
|
||||
public static func capture(
|
||||
configuration: RDEPUBReaderConfiguration,
|
||||
brightness: CGFloat
|
||||
|
||||
@@ -1,12 +1,27 @@
|
||||
import UIKit
|
||||
|
||||
// MARK: - 设置面板
|
||||
|
||||
/// EPUB 阅读器的设置面板控制器
|
||||
/// 提供亮度、字号、行距、翻页方式、主题的调节功能
|
||||
/// 支持 iOS 13+ SF Symbols 和低版本文字回退
|
||||
final class RDEPUBReaderSettingsViewController: UIViewController {
|
||||
// MARK: 回调闭包
|
||||
|
||||
/// 亮度变化回调(0.0 ~ 1.0)
|
||||
var onBrightnessChange: ((CGFloat) -> Void)?
|
||||
/// 字号变化回调(pt 值)
|
||||
var onFontSizeChange: ((CGFloat) -> Void)?
|
||||
/// 行距倍数变化回调
|
||||
var onLineHeightChange: ((CGFloat) -> Void)?
|
||||
/// 翻页模式变化回调
|
||||
var onDisplayTypeChange: ((RDReaderView.DisplayType) -> Void)?
|
||||
/// 主题变化回调
|
||||
var onThemeChange: ((RDEPUBReaderTheme) -> Void)?
|
||||
|
||||
// MARK: 主题预设枚举
|
||||
|
||||
/// 设置面板中的主题预设枚举,用于按钮显示和主题切换
|
||||
private enum ThemePreset: Int, CaseIterable {
|
||||
case light
|
||||
case yellow
|
||||
@@ -60,7 +75,9 @@ final class RDEPUBReaderSettingsViewController: UIViewController {
|
||||
}()
|
||||
private var themeButtons: [UIButton] = []
|
||||
|
||||
/// 行距选项对应的倍数值(紧凑 1.3、标准 1.6、宽松 1.9)
|
||||
private let lineHeightValues: [CGFloat] = [1.3, 1.6, 1.9]
|
||||
/// 当前配置的本地副本,修改后通过回调通知控制器
|
||||
private var currentConfiguration: RDEPUBReaderConfiguration
|
||||
|
||||
init(configuration: RDEPUBReaderConfiguration, brightness: CGFloat) {
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
import Foundation
|
||||
|
||||
// MARK: - 目录条目
|
||||
|
||||
/// 展平后的目录条目结构体
|
||||
/// 用于目录面板展示,将树形目录展平为一维列表,通过 depth 字段保留层级关系
|
||||
public struct RDEPUBReaderTableOfContentsItem: Equatable {
|
||||
/// 章节标题
|
||||
public var title: String
|
||||
/// 章节的 href(相对于 EPUB 容器根目录)
|
||||
public var href: String
|
||||
/// 目录层级深度(0 为顶级章节),用于列表缩进显示
|
||||
public var depth: Int
|
||||
/// 该章节所在页码(从 1 开始),nil 表示无法确定
|
||||
public var pageNumber: Int?
|
||||
|
||||
public init(title: String, href: String, depth: Int, pageNumber: Int? = nil) {
|
||||
|
||||
@@ -1,13 +1,37 @@
|
||||
import UIKit
|
||||
|
||||
// MARK: - 阅读器主题
|
||||
|
||||
/// EPUB 阅读器主题配置结构体
|
||||
/// 定义阅读器内容区域和工具栏的颜色方案
|
||||
/// 内置浅色、深色、黄色、绿色、粉色、蓝色六套预设主题
|
||||
public struct RDEPUBReaderTheme: Equatable {
|
||||
// MARK: 内容区颜色
|
||||
|
||||
/// 内容区域的背景色
|
||||
public var contentBackgroundColor: UIColor
|
||||
/// 内容区域的正文文字颜色
|
||||
public var contentTextColor: UIColor
|
||||
|
||||
// MARK: 工具栏颜色
|
||||
|
||||
/// 工具栏背景色
|
||||
public var toolBackgroundColor: UIColor
|
||||
/// 工具栏控件文字/图标颜色
|
||||
public var toolControlTextColor: UIColor
|
||||
/// 工具栏控件未选中状态的边框颜色
|
||||
public var toolControlBorderUnselectColor: UIColor
|
||||
/// 工具栏中分隔线的颜色
|
||||
public var toolLineColor: UIColor
|
||||
|
||||
/// 初始化主题配置
|
||||
/// - Parameters:
|
||||
/// - contentBackgroundColor: 内容区背景色
|
||||
/// - contentTextColor: 内容区文字颜色
|
||||
/// - toolBackgroundColor: 工具栏背景色
|
||||
/// - toolControlTextColor: 工具栏控件颜色
|
||||
/// - toolControlBorderUnselectColor: 控件未选中边框色
|
||||
/// - toolLineColor: 分隔线颜色
|
||||
public init(
|
||||
contentBackgroundColor: UIColor,
|
||||
contentTextColor: UIColor,
|
||||
@@ -24,6 +48,9 @@ public struct RDEPUBReaderTheme: Equatable {
|
||||
self.toolLineColor = toolLineColor
|
||||
}
|
||||
|
||||
// MARK: 内置预设主题
|
||||
|
||||
/// 浅色主题:白底黑字,适合日间阅读
|
||||
public static let light = RDEPUBReaderTheme(
|
||||
contentBackgroundColor: .white,
|
||||
contentTextColor: .black,
|
||||
@@ -33,6 +60,7 @@ public struct RDEPUBReaderTheme: Equatable {
|
||||
toolLineColor: UIColor.lightGray.withAlphaComponent(0.5)
|
||||
)
|
||||
|
||||
/// 深色主题:黑底白字,适合夜间阅读
|
||||
public static let dark = RDEPUBReaderTheme(
|
||||
contentBackgroundColor: .black,
|
||||
contentTextColor: .white,
|
||||
@@ -42,6 +70,7 @@ public struct RDEPUBReaderTheme: Equatable {
|
||||
toolLineColor: UIColor.lightGray.withAlphaComponent(0.5)
|
||||
)
|
||||
|
||||
/// 黄色护眼主题:暖色调背景,减少蓝光刺激
|
||||
public static let yellow = RDEPUBReaderTheme(
|
||||
contentBackgroundColor: UIColor(red: 0.89, green: 0.87, blue: 0.79, alpha: 1),
|
||||
contentTextColor: .black,
|
||||
@@ -51,6 +80,7 @@ public struct RDEPUBReaderTheme: Equatable {
|
||||
toolLineColor: UIColor.lightGray.withAlphaComponent(0.5)
|
||||
)
|
||||
|
||||
/// 绿色护眼主题:模拟纸质书的柔和绿色背景
|
||||
public static let green = RDEPUBReaderTheme(
|
||||
contentBackgroundColor: UIColor(red: 0.87, green: 0.91, blue: 0.82, alpha: 1),
|
||||
contentTextColor: .black,
|
||||
@@ -60,6 +90,7 @@ public struct RDEPUBReaderTheme: Equatable {
|
||||
toolLineColor: UIColor.lightGray.withAlphaComponent(0.5)
|
||||
)
|
||||
|
||||
/// 粉色主题:柔和粉色背景
|
||||
public static let pink = RDEPUBReaderTheme(
|
||||
contentBackgroundColor: UIColor(red: 1, green: 0.89, blue: 0.91, alpha: 1),
|
||||
contentTextColor: .black,
|
||||
@@ -69,6 +100,7 @@ public struct RDEPUBReaderTheme: Equatable {
|
||||
toolLineColor: UIColor.lightGray.withAlphaComponent(0.5)
|
||||
)
|
||||
|
||||
/// 蓝色主题:冷色调蓝色背景
|
||||
public static let blue = RDEPUBReaderTheme(
|
||||
contentBackgroundColor: UIColor(red: 0.8, green: 0.84, blue: 0.89, alpha: 1),
|
||||
contentTextColor: .black,
|
||||
@@ -79,11 +111,15 @@ public struct RDEPUBReaderTheme: Equatable {
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - CSS 颜色转换
|
||||
|
||||
extension RDEPUBReaderTheme {
|
||||
/// 将内容区背景色转换为 CSS 颜色字符串,注入 EPUB 的 HTML 中
|
||||
var themeBackgroundColorCSS: String {
|
||||
contentBackgroundColor.ss_cssString
|
||||
}
|
||||
|
||||
/// 将内容区文字颜色转换为 CSS 颜色字符串,注入 EPUB 的 HTML 中
|
||||
var themeTextColorCSS: String {
|
||||
contentTextColor.ss_cssString
|
||||
}
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import UIKit
|
||||
|
||||
// MARK: - 工具栏基类
|
||||
|
||||
/// 阅读器工具栏的基类
|
||||
/// 提供分隔线和主题适配的通用逻辑,子类只需关注具体功能按钮
|
||||
open class RDEPUBReaderToolView: UIView {
|
||||
/// 分隔线视图
|
||||
private let lineView = UIView()
|
||||
/// 分隔线高度
|
||||
private let lineHeight: CGFloat = 0.5
|
||||
|
||||
override init(frame: CGRect) {
|
||||
@@ -19,15 +25,24 @@ open class RDEPUBReaderToolView: UIView {
|
||||
lineView.frame = lineFrame(in: bounds)
|
||||
}
|
||||
|
||||
/// 应用主题配色到工具栏
|
||||
/// - Parameter theme: 阅读器主题配置
|
||||
open func apply(theme: RDEPUBReaderTheme) {
|
||||
backgroundColor = .white
|
||||
lineView.backgroundColor = UIColor.lightGray.withAlphaComponent(0.5)
|
||||
}
|
||||
|
||||
/// 计算分隔线的位置和尺寸,子类可重写以调整分隔线位置(如顶部或底部)
|
||||
/// - Parameter bounds: 工具栏的 bounds
|
||||
/// - Returns: 分隔线的 frame
|
||||
open func lineFrame(in bounds: CGRect) -> CGRect {
|
||||
CGRect(x: 0, y: 0, width: bounds.width, height: lineHeight)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 工具栏按钮
|
||||
|
||||
/// 阅读器工具栏专用按钮
|
||||
/// 继承自 UIButton,用于工具栏中的功能按钮
|
||||
final class RDEPUBReaderTintButton: UIButton {
|
||||
}
|
||||
@@ -1,7 +1,14 @@
|
||||
import UIKit
|
||||
|
||||
// MARK: - 顶部工具栏
|
||||
|
||||
/// 阅读器顶部工具栏
|
||||
/// 显示书名、返回按钮和书签按钮
|
||||
/// 位于阅读内容上方,支持 iOS 13+ SF Symbols 和低版本文字回退
|
||||
public final class RDEPUBReaderTopToolView: RDEPUBReaderToolView {
|
||||
/// 返回按钮点击回调
|
||||
var onBack: (() -> Void)?
|
||||
/// 书签按钮点击回调
|
||||
var onToggleBookmark: (() -> Void)?
|
||||
|
||||
private let backButton = RDEPUBReaderTintButton(type: .system)
|
||||
|
||||
@@ -1,21 +1,41 @@
|
||||
import UIKit
|
||||
|
||||
// MARK: - 覆盖层装饰类型
|
||||
|
||||
/// 覆盖层装饰的数据结构
|
||||
/// 表示一个需要绘制在文本上方或下方的视觉装饰(高亮、搜索结果、选区等)
|
||||
struct RDEPUBTextOverlayDecoration {
|
||||
/// 装饰类型枚举
|
||||
enum Kind: String {
|
||||
/// 文本选区(蓝色半透明)
|
||||
case selection
|
||||
/// 用户高亮标注
|
||||
case highlight
|
||||
/// 用户划线标注
|
||||
case underline
|
||||
/// 搜索匹配项(普通)
|
||||
case search
|
||||
/// 搜索匹配项(当前高亮)
|
||||
case activeSearch
|
||||
/// 定位指示(跳转到位置时的动画目标)
|
||||
case locate
|
||||
}
|
||||
|
||||
/// 装饰类型
|
||||
var kind: Kind
|
||||
/// 装饰对应的绝对文本范围
|
||||
var absoluteRange: NSRange
|
||||
/// 装饰的绘制矩形数组(每行一个矩形)
|
||||
var rects: [CGRect]
|
||||
/// 装饰的颜色
|
||||
var color: UIColor
|
||||
}
|
||||
|
||||
// MARK: - 选择覆盖层视图
|
||||
|
||||
/// 文本选择和装饰的覆盖层绘制视图
|
||||
/// 位于文本内容上方,负责绘制选区、高亮、搜索结果等视觉效果
|
||||
/// 使用 Core Graphics 直接绘制,支持填充矩形和下划线两种绘制模式
|
||||
final class RDEPUBSelectionOverlayView: UIView {
|
||||
private(set) var page: RDEPUBTextPage?
|
||||
private var snapshot: RDEPUBPageLayoutSnapshot?
|
||||
|
||||
@@ -5,12 +5,22 @@ import Foundation
|
||||
import DTCoreText
|
||||
#endif
|
||||
|
||||
// MARK: - 文本内容视图代理
|
||||
|
||||
/// 文本内容视图的代理协议
|
||||
/// 通知控制器文本选择变化和选择菜单操作
|
||||
protocol RDEPUBTextContentViewDelegate: AnyObject {
|
||||
/// 用户选中文本发生变化时调用
|
||||
func textContentView(_ contentView: RDEPUBTextContentView, didChangeSelection selection: RDEPUBSelection?)
|
||||
/// 用户从选择菜单中触发操作(拷贝/高亮/批注)
|
||||
func textContentView(_ contentView: RDEPUBTextContentView, didRequestSelectionAction action: RDEPUBAnnotationMenuAction)
|
||||
}
|
||||
|
||||
// MARK: - 可选中文本视图
|
||||
|
||||
/// 自定义 UITextView,替换系统默认的 UIMenuItem 为自定义操作(拷贝、高亮、批注)
|
||||
final class RDEPUBSelectableTextView: UITextView {
|
||||
/// 选择菜单操作回调
|
||||
var onSelectionAction: ((RDEPUBAnnotationMenuAction) -> Void)?
|
||||
|
||||
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
|
||||
@@ -37,6 +47,10 @@ final class RDEPUBSelectableTextView: UITextView {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - DTCoreText 直接绘制视图
|
||||
|
||||
/// 基于 DTCoreText 的 Core Text 直接绘制视图
|
||||
/// 将 DTCoreText 的排版结果直接绘制到 UIView 上,跳过 UITextView 的间接渲染
|
||||
#if canImport(DTCoreText)
|
||||
final class RDEPUBDirectCoreTextPageView: UIView {
|
||||
var layoutFrame: DTCoreTextLayoutFrame? {
|
||||
@@ -73,6 +87,14 @@ final class RDEPUBDirectCoreTextPageView: UIView {
|
||||
}
|
||||
#endif
|
||||
|
||||
// MARK: - 文本内容视图
|
||||
|
||||
/// EPUB 流式排版的文本内容视图
|
||||
/// 支持两种渲染路径:
|
||||
/// 1. DTCoreText 路径:直接绘制到 CoreText 视图,支持精确的排版控制
|
||||
/// 2. 回退路径:通过 UITextView 的 attributedText 渲染
|
||||
///
|
||||
/// 内置能力:高亮覆盖、搜索高亮、文本选择、长按菜单、封面图显示
|
||||
final class RDEPUBTextContentView: UIView {
|
||||
private var contentInsets: UIEdgeInsets = .zero
|
||||
private var currentPage: RDEPUBTextPage?
|
||||
|
||||
@@ -1,14 +1,29 @@
|
||||
import UIKit
|
||||
|
||||
// MARK: - Web 内容视图代理
|
||||
|
||||
/// Web 内容视图的代理协议
|
||||
/// 将 WebView 中的事件(位置更新、文本选择、链接点击等)转发给控制器
|
||||
protocol RDEPUBWebContentViewDelegate: AnyObject {
|
||||
/// WebView 中阅读位置更新时调用
|
||||
func epubWebContentView(_ contentView: RDEPUBWebContentView, didUpdateLocation location: RDEPUBLocation, spineIndex: Int)
|
||||
/// WebView 中文本选择变化时调用
|
||||
func epubWebContentView(_ contentView: RDEPUBWebContentView, didChangeSelection selection: RDEPUBSelection?, spineIndex: Int)
|
||||
/// 用户从选择菜单中触发操作
|
||||
func epubWebContentView(_ contentView: RDEPUBWebContentView, didRequestSelectionAction action: RDEPUBAnnotationMenuAction)
|
||||
/// 用户点击内部链接(章节跳转)
|
||||
func epubWebContentView(_ contentView: RDEPUBWebContentView, didActivateInternalLink location: RDEPUBLocation, fromSpineIndex: Int)
|
||||
/// 用户点击外部链接
|
||||
func epubWebContentView(_ contentView: RDEPUBWebContentView, didActivateExternalLink url: URL)
|
||||
/// WebView 中 JavaScript 执行出错
|
||||
func epubWebContentView(_ contentView: RDEPUBWebContentView, didLogJavaScriptError message: String)
|
||||
}
|
||||
|
||||
// MARK: - Web 内容视图
|
||||
|
||||
/// EPUB 固定布局和 Web 渲染路径的内容视图
|
||||
/// 包装 RDEPUBWebView,提供页码标签和加载/释放资源的控制
|
||||
/// 适用于固定布局 EPUB 和不支持 Native Text 的场景
|
||||
final class RDEPUBWebContentView: UIView {
|
||||
weak var delegate: RDEPUBWebContentViewDelegate?
|
||||
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
//
|
||||
// RDURLReaderController.swift
|
||||
// RDReaderDemo
|
||||
//
|
||||
// 文件职责:URL 阅读器入口控制器,根据文件类型自动选择阅读器。
|
||||
// 该文件是 ReadViewSDK 的最简使用入口:
|
||||
// - .epub 文件:直接创建 RDEPUBReaderController
|
||||
// - 其他文本文件:通过 RDPlainTextBookBuilder 分页后创建 RDEPUBReaderController
|
||||
// - 分页失败时回退到纯文本 UITextView 展示
|
||||
//
|
||||
// 架构位置:RDReaderView 四层架构中第四层(读者 UI)的入口控制器。
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
/// URL 阅读器入口控制器
|
||||
/// 接收一个书籍文件 URL,根据文件类型自动创建对应的阅读器控制器:
|
||||
/// - EPUB 文件:直接使用 RDEPUBReaderController
|
||||
/// - 纯文本文件:先通过 RDPlainTextBookBuilder 分页,再使用 RDEPUBReaderController
|
||||
/// - 分页失败:回退到纯文本 UITextView 展示
|
||||
///
|
||||
/// 使用方式:
|
||||
/// ```swift
|
||||
/// let controller = RDURLReaderController(bookURL: fileURL)
|
||||
/// navigationController?.pushViewController(controller, animated: true)
|
||||
/// ```
|
||||
public final class RDURLReaderController: UIViewController {
|
||||
/// 书籍文件 URL
|
||||
private let bookURL: URL
|
||||
/// EPUB 阅读器配置(字体、行距、主题等)
|
||||
private let epubConfiguration: RDEPUBReaderConfiguration
|
||||
/// 内嵌的阅读器控制器
|
||||
private var embeddedController: UIViewController?
|
||||
|
||||
/// 初始化方法
|
||||
/// - Parameters:
|
||||
/// - bookURL: 书籍文件的本地 URL
|
||||
/// - epubConfiguration: EPUB 阅读器配置,默认使用标准配置
|
||||
public init(
|
||||
bookURL: URL,
|
||||
epubConfiguration: RDEPUBReaderConfiguration = RDEPUBReaderConfiguration()
|
||||
) {
|
||||
self.bookURL = bookURL
|
||||
self.epubConfiguration = epubConfiguration
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
/// 视图加载完成后,设置背景色、标题并嵌入阅读器控制器
|
||||
public override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
view.backgroundColor = .systemBackground
|
||||
title = bookURL.deletingPathExtension().lastPathComponent
|
||||
embedReaderController()
|
||||
}
|
||||
|
||||
/// 切换阅读器的翻页模式(Demo 用)
|
||||
/// - Parameter displayType: 目标翻页模式
|
||||
public func applyDemoDisplayType(_ displayType: RDReaderView.DisplayType) {
|
||||
readerController?.configuration.displayType = displayType
|
||||
logDemoState(prefix: "display=\(displayType.demoArgumentValue)")
|
||||
}
|
||||
|
||||
/// 跳转到指定页码(Demo 用)
|
||||
/// - Parameters:
|
||||
/// - pageNumber: 目标页码
|
||||
/// - animated: 是否动画过渡
|
||||
/// - Returns: 是否成功跳转
|
||||
@discardableResult
|
||||
public func goToDemoPage(_ pageNumber: Int, animated: Bool = false) -> Bool {
|
||||
let moved = readerController?.go(toPageNumber: pageNumber, animated: animated) ?? false
|
||||
if moved {
|
||||
logDemoState(prefix: "page=\(pageNumber)")
|
||||
}
|
||||
return moved
|
||||
}
|
||||
|
||||
/// 执行翻页模式切换序列(Demo 用)
|
||||
/// 按照指定顺序和延迟依次切换翻页模式
|
||||
/// - Parameters:
|
||||
/// - displayTypes: 翻页模式数组
|
||||
/// - initialPageNumber: 可选的初始跳转页码
|
||||
/// - stepDelay: 每步之间的延迟时间(秒)
|
||||
public func runDemoDisplaySequence(
|
||||
_ displayTypes: [RDReaderView.DisplayType],
|
||||
initialPageNumber: Int? = nil,
|
||||
stepDelay: TimeInterval = 1.0
|
||||
) {
|
||||
let normalizedDelay = max(stepDelay, 0.1)
|
||||
if let initialPageNumber {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + normalizedDelay) { [weak self] in
|
||||
_ = self?.goToDemoPage(initialPageNumber)
|
||||
}
|
||||
}
|
||||
|
||||
for (index, displayType) in displayTypes.enumerated() {
|
||||
let delay = normalizedDelay * Double(index + 1 + (initialPageNumber == nil ? 0 : 1))
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
|
||||
self?.applyDemoDisplayType(displayType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 嵌入阅读器控制器到当前视图层级
|
||||
/// 根据文件类型选择合适的阅读器控制器,并通过 Child View Controller 方式嵌入
|
||||
private func embedReaderController() {
|
||||
let controller: UIViewController
|
||||
if bookURL.pathExtension.lowercased() == "epub" {
|
||||
controller = RDEPUBReaderController(
|
||||
epubURL: bookURL,
|
||||
configuration: epubConfiguration
|
||||
)
|
||||
} else {
|
||||
let bookIdentifier = bookURL.lastPathComponent
|
||||
let bookTitle = bookURL.deletingPathExtension().lastPathComponent
|
||||
let pageSize = currentTextPageSize()
|
||||
let renderStyle = currentTextRenderStyle()
|
||||
let builder = RDPlainTextBookBuilder()
|
||||
if let textBook = try? builder.build(textFileURL: bookURL, pageSize: pageSize, style: renderStyle) {
|
||||
controller = RDEPUBReaderController(
|
||||
textBook: textBook,
|
||||
bookIdentifier: bookIdentifier,
|
||||
title: bookTitle,
|
||||
textFileURL: bookURL,
|
||||
configuration: epubConfiguration
|
||||
)
|
||||
} else {
|
||||
// 分页失败时回退到纯文本展示
|
||||
let fallback = UIViewController()
|
||||
let textView = UITextView()
|
||||
textView.isEditable = false
|
||||
textView.text = rd_decodeTextFile(url: bookURL)
|
||||
fallback.view = textView
|
||||
controller = fallback
|
||||
}
|
||||
}
|
||||
embeddedController = controller
|
||||
addChild(controller)
|
||||
controller.view.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(controller.view)
|
||||
NSLayoutConstraint.activate([
|
||||
controller.view.topAnchor.constraint(equalTo: view.topAnchor),
|
||||
controller.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
controller.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
controller.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||
])
|
||||
controller.didMove(toParent: self)
|
||||
}
|
||||
|
||||
/// 获取内嵌的 EPUB 阅读器控制器(如果存在)
|
||||
private var readerController: RDEPUBReaderController? {
|
||||
embeddedController as? RDEPUBReaderController
|
||||
}
|
||||
|
||||
/// 输出当前阅读器状态日志(Demo 调试用)
|
||||
private func logDemoState(prefix: String) {
|
||||
guard let readerController else { return }
|
||||
let location = readerController.currentLocation
|
||||
let href = location?.href ?? "nil"
|
||||
let progression = location.map { String(format: "%.4f", $0.navigationProgression) } ?? "nil"
|
||||
let page = readerController.currentPageNumber.map(String.init) ?? "nil"
|
||||
print("[ReadViewDemo] automation \(prefix) -> page \(page) href \(href) progression \(progression)")
|
||||
}
|
||||
|
||||
/// 计算文本分页的页面尺寸
|
||||
/// 基于屏幕尺寸和配置的内边距
|
||||
private func currentTextPageSize() -> CGSize {
|
||||
let viewportSize = UIScreen.main.bounds.size
|
||||
let insets = epubConfiguration.reflowableContentInsets
|
||||
return CGSize(
|
||||
width: max(viewportSize.width - insets.left - insets.right, 1),
|
||||
height: max(viewportSize.height - insets.top - insets.bottom, 1)
|
||||
)
|
||||
}
|
||||
|
||||
/// 根据当前配置生成文本渲染样式
|
||||
private func currentTextRenderStyle() -> RDEPUBTextRenderStyle {
|
||||
let font = UIFont.systemFont(ofSize: epubConfiguration.fontSize)
|
||||
let lineSpacing = max(font.lineHeight * (epubConfiguration.lineHeightMultiple - 1), 4)
|
||||
return RDEPUBTextRenderStyle(
|
||||
font: font,
|
||||
lineSpacing: lineSpacing,
|
||||
textColor: epubConfiguration.theme.contentTextColor,
|
||||
backgroundColor: epubConfiguration.theme.contentBackgroundColor
|
||||
)
|
||||
}
|
||||
|
||||
/// 解码文本文件内容
|
||||
/// 尝试 UTF-8 编码,失败则尝试 GBK 和 GB2312 编码
|
||||
private func rd_decodeTextFile(url: URL) -> String {
|
||||
if let content = try? NSString(contentsOf: url, encoding: String.Encoding.utf8.rawValue) as String {
|
||||
return content
|
||||
}
|
||||
if let content = try? NSString(contentsOf: url, encoding: 0x80000632) as String {
|
||||
return content
|
||||
}
|
||||
if let content = try? NSString(contentsOf: url, encoding: 0x80000631) as String {
|
||||
return content
|
||||
}
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
/// 翻页模式扩展:提供 Demo 命令行参数值
|
||||
private extension RDReaderView.DisplayType {
|
||||
/// 返回翻页模式对应的 Demo 命令行参数字符串
|
||||
var demoArgumentValue: String {
|
||||
switch self {
|
||||
case .pageCurl:
|
||||
return "pageCurl"
|
||||
case .horizontalScroll:
|
||||
return "horizontalScroll"
|
||||
case .verticalScroll:
|
||||
return "verticalScroll"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user