136 lines
4.6 KiB
Swift
136 lines
4.6 KiB
Swift
import Foundation
|
||
|
||
public struct RDEPUBLocation: Codable, Equatable {
|
||
/// 书籍唯一标识符,用于隔离不同书的阅读进度
|
||
public var bookIdentifier: String?
|
||
/// OPF 相对路径,对应 spine 中的资源文件
|
||
public var href: String
|
||
/// 视口起始位置在章节中的相对进度 [0, 1]
|
||
public var progression: Double
|
||
/// 视口末尾位置在章节中的相对进度 [0, 1](可选)
|
||
public var lastProgression: Double?
|
||
/// 锚点标识符(如 #section1),用于精确跳转
|
||
public var fragment: String?
|
||
/// 文本范围锚点(用于文本 EPUB 的高亮定位)
|
||
public var rangeAnchor: RDEPUBTextRangeAnchor?
|
||
|
||
public init(
|
||
bookIdentifier: String? = nil,
|
||
href: String,
|
||
progression: Double,
|
||
lastProgression: Double? = nil,
|
||
fragment: String? = nil,
|
||
rangeAnchor: RDEPUBTextRangeAnchor? = nil
|
||
) {
|
||
self.bookIdentifier = bookIdentifier
|
||
self.href = href
|
||
self.progression = Self.clamp(progression)
|
||
self.lastProgression = lastProgression.map(Self.clamp)
|
||
self.fragment = fragment?.nilIfEmpty
|
||
self.rangeAnchor = rangeAnchor
|
||
}
|
||
|
||
/// 导航用的中间进度值,取 progression 和 lastProgression 的中点
|
||
/// 用于在字号/横竖屏变化后估算最接近的页号
|
||
public var navigationProgression: Double {
|
||
let end = lastProgression ?? progression
|
||
if end.isNaN || end.isInfinite {
|
||
return progression
|
||
}
|
||
return Self.clamp((progression + end) / 2.0)
|
||
}
|
||
|
||
/// 将进度值限制在 [0, 1] 范围内,防止越界
|
||
private static func clamp(_ value: Double) -> Double {
|
||
guard value.isFinite else { return 0 }
|
||
return min(1, max(0, value))
|
||
}
|
||
}
|
||
|
||
/// 视口中的单个资源信息,表示当前屏幕可见区域覆盖的某个 spine 资源
|
||
public struct RDEPUBViewportResource: Codable, Equatable {
|
||
/// 资源路径(相对于 OPF 目录)
|
||
public var href: String
|
||
/// 对应的 spine 索引
|
||
public var spineIndex: Int
|
||
/// 该资源在视口中的起始进度 [0, 1]
|
||
public var progression: Double
|
||
/// 该资源在视口中的结束进度 [0, 1]
|
||
public var lastProgression: Double?
|
||
/// 锚点标识符
|
||
public var fragment: String?
|
||
|
||
public init(
|
||
href: String,
|
||
spineIndex: Int,
|
||
progression: Double,
|
||
lastProgression: Double? = nil,
|
||
fragment: String? = nil
|
||
) {
|
||
self.href = href
|
||
self.spineIndex = spineIndex
|
||
self.progression = progression
|
||
self.lastProgression = lastProgression
|
||
self.fragment = fragment
|
||
}
|
||
}
|
||
|
||
/// 视口模型,描述当前屏幕可见区域的完整状态
|
||
/// 可能跨越多个 spine 资源(如分栏布局时)
|
||
public struct RDEPUBViewport: Codable, Equatable {
|
||
/// 当前视口覆盖的资源列表(可能有多个,如分栏或跨页场景)
|
||
public var resources: [RDEPUBViewportResource]
|
||
/// 当前可见的页码
|
||
public var visiblePageNumber: Int
|
||
/// 当前章节索引
|
||
public var chapterIndex: Int?
|
||
/// 是否为固定版式内容
|
||
public var isFixedLayout: Bool
|
||
|
||
public init(
|
||
resources: [RDEPUBViewportResource],
|
||
visiblePageNumber: Int,
|
||
chapterIndex: Int? = nil,
|
||
isFixedLayout: Bool
|
||
) {
|
||
self.resources = resources
|
||
self.visiblePageNumber = visiblePageNumber
|
||
self.chapterIndex = chapterIndex
|
||
self.isFixedLayout = isFixedLayout
|
||
}
|
||
}
|
||
|
||
/// 阅读上下文,聚合了位置、视口和页码等完整阅读状态
|
||
/// 由 RDEPUBReadingSession 维护,是翻页容器层获取当前阅读状态的主要数据源
|
||
public struct RDEPUBReadingContext: Codable, Equatable {
|
||
/// 当前阅读位置(href + progression)
|
||
public var location: RDEPUBLocation
|
||
/// 当前视口状态
|
||
public var viewport: RDEPUBViewport
|
||
/// 当前扁平页码(跨章节累计)
|
||
public var pageNumber: Int
|
||
/// 当前章节索引
|
||
public var chapterIndex: Int?
|
||
|
||
public init(
|
||
location: RDEPUBLocation,
|
||
viewport: RDEPUBViewport,
|
||
pageNumber: Int,
|
||
chapterIndex: Int? = nil
|
||
) {
|
||
self.location = location
|
||
self.viewport = viewport
|
||
self.pageNumber = pageNumber
|
||
self.chapterIndex = chapterIndex
|
||
}
|
||
}
|
||
|
||
/// 文本选择模型,由 JS 桥接 ssReaderSelectionChanged 消息触发创建
|
||
|
||
private extension String {
|
||
var nilIfEmpty: String? {
|
||
let trimmed = trimmingCharacters(in: .whitespacesAndNewlines)
|
||
return trimmed.isEmpty ? nil : trimmed
|
||
}
|
||
}
|