feat: EPUB阅读器搜索、注释、CFI模块及大书远距跳转优化
- 实现EPUB阅读器搜索功能及选中注释功能 - 优化CFI模块,修复代码审查发现的11个问题 - 实现大书远距目录跳转与后台补全优化方案 - 优化设置面板与章节运行时联动 - 重构及大量改进优化
This commit is contained in:
@@ -1,26 +1,20 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
// MARK: - 自定义 NSAttributedString 属性(对齐 WXRead 的 com.weread.highlight / com.weread.underline)
|
||||
|
||||
/// 高亮自定义属性名,注入到 NSAttributedString 中供 CoreText 绘制时读取
|
||||
/// 对齐 WXRead 的 kWRHighlightAttributeName = "com.weread.highlight"
|
||||
public let kRDEPUBHighlightAttributeName = NSAttributedString.Key("com.rdreader.highlight")
|
||||
|
||||
/// 下划线自定义属性名
|
||||
/// 对齐 WXRead 的 kWRUnderlineAttributeName = "com.weread.underline"
|
||||
public let kRDEPUBUnderlineAttributeName = NSAttributedString.Key("com.rdreader.underline")
|
||||
|
||||
public struct RDEPUBSelection: Codable, Equatable {
|
||||
/// 书籍标识符
|
||||
|
||||
public var bookIdentifier: String?
|
||||
/// 选择发生的位置
|
||||
|
||||
public var location: RDEPUBLocation
|
||||
/// 选中的文本内容
|
||||
|
||||
public var text: String
|
||||
/// DOM Range 信息(JSON 序列化字符串,用于精确重建选区)
|
||||
|
||||
public var rangeInfo: String?
|
||||
/// 选择创建时间
|
||||
|
||||
public var createdAt: Date
|
||||
|
||||
public init(
|
||||
@@ -37,51 +31,38 @@ public struct RDEPUBSelection: Codable, Equatable {
|
||||
self.createdAt = createdAt
|
||||
}
|
||||
|
||||
/// 判断是否为空选择(无文本内容)
|
||||
public var isEmpty: Bool {
|
||||
text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
||||
}
|
||||
}
|
||||
|
||||
/// 高亮样式类型
|
||||
/// - highlight: 背景色高亮
|
||||
/// - underline: 下划线标记
|
||||
public enum RDEPUBHighlightStyle: String, Codable {
|
||||
case highlight
|
||||
case underline
|
||||
}
|
||||
|
||||
/// 统一标注类型,对标 WXRead 的 WRBookmark 语义分层。
|
||||
public enum RDEPUBAnnotationKind: String, Codable, Equatable {
|
||||
case bookmark
|
||||
case highlight
|
||||
case underline
|
||||
}
|
||||
|
||||
/// 标注菜单操作类型,用于用户长按选中文本后的操作选项
|
||||
public enum RDEPUBAnnotationMenuAction: Equatable {
|
||||
case copy
|
||||
case highlight
|
||||
case annotate
|
||||
}
|
||||
|
||||
/// 文本偏移范围信息,用于文本 EPUB 的高亮精确定位
|
||||
/// 使用字符偏移量而非 DOM Range,适用于 DTCoreText 渲染的纯文本内容
|
||||
public struct RDEPUBTextOffsetRangeInfo: Codable, Equatable {
|
||||
/// 类型标识符,固定为 "text-offset"
|
||||
|
||||
public var kind: String
|
||||
/// 资源路径
|
||||
|
||||
public var href: String
|
||||
/// 起始字符偏移量
|
||||
|
||||
public var start: Int
|
||||
/// 结束字符偏移量
|
||||
|
||||
public var end: Int
|
||||
|
||||
/// 初始化文本偏移范围
|
||||
/// - Parameters:
|
||||
/// - href: 资源路径
|
||||
/// - start: 起始字符偏移量
|
||||
/// - end: 结束字符偏移量
|
||||
public init(href: String, start: Int, end: Int) {
|
||||
self.kind = "text-offset"
|
||||
self.href = href
|
||||
@@ -89,19 +70,16 @@ public struct RDEPUBTextOffsetRangeInfo: Codable, Equatable {
|
||||
self.end = end
|
||||
}
|
||||
|
||||
/// 转换为 NSRange(用于 NSAttributedString 操作)
|
||||
public var nsRange: NSRange? {
|
||||
guard end > start else { return nil }
|
||||
return NSRange(location: start, length: end - start)
|
||||
}
|
||||
|
||||
/// 序列化为 JSON 字符串
|
||||
public func jsonString() -> String? {
|
||||
guard let data = try? JSONEncoder().encode(self) else { return nil }
|
||||
return String(data: data, encoding: .utf8)
|
||||
}
|
||||
|
||||
/// 从 JSON 字符串反序列化,校验 kind 和范围有效性
|
||||
public static func decode(from string: String?) -> RDEPUBTextOffsetRangeInfo? {
|
||||
guard let string,
|
||||
let data = string.data(using: .utf8),
|
||||
@@ -114,25 +92,24 @@ public struct RDEPUBTextOffsetRangeInfo: Codable, Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
/// 文本分页截断原因,描述为什么在当前位置分页
|
||||
public struct RDEPUBHighlight: Codable, Equatable {
|
||||
/// 高亮唯一标识符
|
||||
|
||||
public var id: String
|
||||
/// 书籍标识符
|
||||
|
||||
public var bookIdentifier: String?
|
||||
/// 高亮发生的位置
|
||||
|
||||
public var location: RDEPUBLocation
|
||||
/// 高亮的文本内容
|
||||
|
||||
public var text: String
|
||||
/// 范围信息(Web EPUB 为 DOM Range JSON,文本 EPUB 为字符偏移 JSON)
|
||||
|
||||
public var rangeInfo: String?
|
||||
/// 高亮样式(背景色高亮或下划线)
|
||||
|
||||
public var style: RDEPUBHighlightStyle
|
||||
/// 高亮颜色(CSS 颜色值,如 "#F8E16C")
|
||||
|
||||
public var color: String
|
||||
/// 用户备注
|
||||
|
||||
public var note: String?
|
||||
/// 创建时间
|
||||
|
||||
public var createdAt: Date
|
||||
|
||||
public init(
|
||||
@@ -157,18 +134,15 @@ public struct RDEPUBHighlight: Codable, Equatable {
|
||||
self.createdAt = createdAt
|
||||
}
|
||||
|
||||
/// 是否包含用户备注
|
||||
public var hasNote: Bool {
|
||||
note?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false
|
||||
}
|
||||
|
||||
/// 将 CSS hex 颜色转为 UIColor(对齐 WXRead 的 UIColorForHighlightColor,35% alpha)
|
||||
public var uiColor: UIColor {
|
||||
UIColor(rdHexString: color, alpha: 0.35)
|
||||
?? UIColor(red: 248 / 255, green: 225 / 255, blue: 108 / 255, alpha: 0.35)
|
||||
}
|
||||
|
||||
/// 编码键,用于 Codable 序列化/反序列化
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case bookIdentifier
|
||||
@@ -181,7 +155,6 @@ public struct RDEPUBHighlight: Codable, Equatable {
|
||||
case createdAt
|
||||
}
|
||||
|
||||
/// 自定义解码器,兼容旧版本数据格式(缺失字段使用默认值)
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
id = try container.decode(String.self, forKey: .id)
|
||||
@@ -200,7 +173,6 @@ public struct RDEPUBHighlight: Codable, Equatable {
|
||||
createdAt = try container.decodeIfPresent(Date.self, forKey: .createdAt) ?? Date()
|
||||
}
|
||||
|
||||
/// 自定义编码器
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(id, forKey: .id)
|
||||
@@ -219,19 +191,18 @@ public struct RDEPUBHighlight: Codable, Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
/// 书签模型,记录用户标记的阅读位置
|
||||
public struct RDEPUBBookmark: Codable, Equatable {
|
||||
/// 书签唯一标识符
|
||||
|
||||
public var id: String
|
||||
/// 书籍标识符
|
||||
|
||||
public var bookIdentifier: String?
|
||||
/// 书签位置
|
||||
|
||||
public var location: RDEPUBLocation
|
||||
/// 章节标题(用于书签列表展示)
|
||||
|
||||
public var chapterTitle: String?
|
||||
/// 用户备注
|
||||
|
||||
public var note: String?
|
||||
/// 创建时间
|
||||
|
||||
public var createdAt: Date
|
||||
|
||||
public init(
|
||||
@@ -250,7 +221,6 @@ public struct RDEPUBBookmark: Codable, Equatable {
|
||||
self.createdAt = createdAt
|
||||
}
|
||||
|
||||
/// 是否包含用户备注
|
||||
public var hasNote: Bool {
|
||||
note?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false
|
||||
}
|
||||
@@ -260,7 +230,6 @@ public struct RDEPUBBookmark: Codable, Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
/// 统一标注模型,对标 WXRead 的 WRBookmark。
|
||||
public struct RDEPUBAnnotation: Codable, Equatable {
|
||||
public var id: String
|
||||
public var bookIdentifier: String?
|
||||
@@ -358,7 +327,6 @@ public struct RDEPUBAnnotation: Codable, Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private extension String {
|
||||
var nilIfEmpty: String? {
|
||||
let trimmed = trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
|
||||
Reference in New Issue
Block a user