Files
ReadViewSDK/Sources/RDReaderView/EPUBCore/Models/RDEPUBReadingLocationModels.swift
T

136 lines
4.6 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}
}