feat: complete large-book pagination runtime and UI coverage
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
import Foundation
|
||||
|
||||
/// BookPageMap 中的单个条目,记录一个章节的轻量元数据。
|
||||
/// 不持有 NSAttributedString,每条约 100 字节。
|
||||
struct RDEPUBBookPageMapEntry {
|
||||
let spineIndex: Int
|
||||
let href: String
|
||||
let title: String
|
||||
/// 该章节的页数
|
||||
let pageCount: Int
|
||||
/// 该章节在全书中的绝对起始页码(从 0 开始)
|
||||
let absolutePageStart: Int
|
||||
/// fragment ID → 字符偏移量映射
|
||||
let fragmentOffsets: [String: Int]
|
||||
}
|
||||
|
||||
/// 全书轻量页码映射:仅存储每章的页数和起始位置,
|
||||
/// 内存成本约 100 字节/章,1000 章 ≈ 100KB。
|
||||
///
|
||||
/// 提供 spineIndex ↔ 绝对页码的双向查询,
|
||||
/// 用于进度条、目录跳转、位置恢复等不依赖内容的场景。
|
||||
struct RDEPUBBookPageMap {
|
||||
let entries: [RDEPUBBookPageMapEntry]
|
||||
/// 按 spineIndex 索引的查找表
|
||||
private let indexBySpine: [Int: Int] // spineIndex -> entries 数组下标
|
||||
/// 全书总页数
|
||||
let totalPages: Int
|
||||
|
||||
init(entries: [RDEPUBBookPageMapEntry]) {
|
||||
self.entries = entries
|
||||
var mapping: [Int: Int] = [:]
|
||||
for (i, entry) in entries.enumerated() {
|
||||
mapping[entry.spineIndex] = i
|
||||
}
|
||||
self.indexBySpine = mapping
|
||||
self.totalPages = entries.last.map { $0.absolutePageStart + $0.pageCount } ?? 0
|
||||
}
|
||||
|
||||
static let empty = RDEPUBBookPageMap(entries: [])
|
||||
|
||||
// MARK: - 查询
|
||||
|
||||
/// spineIndex + 本地页码 → 全书绝对页码
|
||||
func absolutePageIndex(spineIndex: Int, localPageIndex: Int) -> Int? {
|
||||
guard let idx = indexBySpine[spineIndex] else { return nil }
|
||||
let entry = entries[idx]
|
||||
guard localPageIndex >= 0, localPageIndex < entry.pageCount else { return nil }
|
||||
return entry.absolutePageStart + localPageIndex
|
||||
}
|
||||
|
||||
/// 全书绝对页码 → spineIndex
|
||||
func spineIndex(forAbsolutePage absolutePage: Int) -> Int? {
|
||||
guard absolutePage >= 0, absolutePage < totalPages else { return nil }
|
||||
// 二分查找:entries 按 absolutePageStart 有序
|
||||
var lo = 0, hi = entries.count
|
||||
while lo < hi {
|
||||
let mid = lo + (hi - lo) / 2
|
||||
if entries[mid].absolutePageStart <= absolutePage {
|
||||
lo = mid + 1
|
||||
} else {
|
||||
hi = mid
|
||||
}
|
||||
}
|
||||
guard lo > 0 else { return nil }
|
||||
return entries[lo - 1].spineIndex
|
||||
}
|
||||
|
||||
/// 全书绝对页码 → 本地页码(章节内偏移)
|
||||
func localPageIndex(forAbsolutePage absolutePage: Int) -> Int? {
|
||||
guard let si = spineIndex(forAbsolutePage: absolutePage),
|
||||
let idx = indexBySpine[si] else { return nil }
|
||||
let entry = entries[idx]
|
||||
let local = absolutePage - entry.absolutePageStart
|
||||
guard local >= 0, local < entry.pageCount else { return nil }
|
||||
return local
|
||||
}
|
||||
|
||||
/// 获取指定 spineIndex 的条目
|
||||
func entry(forSpineIndex spineIndex: Int) -> RDEPUBBookPageMapEntry? {
|
||||
guard let idx = indexBySpine[spineIndex] else { return nil }
|
||||
return entries[idx]
|
||||
}
|
||||
|
||||
/// 获取指定 spineIndex 在 entries 中的章节序号。
|
||||
func chapterIndex(forSpineIndex spineIndex: Int) -> Int? {
|
||||
indexBySpine[spineIndex]
|
||||
}
|
||||
|
||||
/// 获取指定 spineIndex 的页数
|
||||
func pageCount(forSpineIndex spineIndex: Int) -> Int? {
|
||||
entry(forSpineIndex: spineIndex)?.pageCount
|
||||
}
|
||||
|
||||
/// 全书总章节数
|
||||
var totalChapters: Int { entries.count }
|
||||
|
||||
// MARK: - 构建
|
||||
|
||||
/// Builder:从各章的 pageCount 逐步构建 BookPageMap
|
||||
struct Builder {
|
||||
private var items: [(spineIndex: Int, href: String, title: String, pageCount: Int, fragmentOffsets: [String: Int])] = []
|
||||
|
||||
mutating func add(spineIndex: Int, href: String, title: String, pageCount: Int, fragmentOffsets: [String: Int]) {
|
||||
items.append((spineIndex, href, title, pageCount, fragmentOffsets))
|
||||
}
|
||||
|
||||
func build() -> RDEPUBBookPageMap {
|
||||
// 按 spineIndex 排序
|
||||
let sorted = items.sorted { $0.spineIndex < $1.spineIndex }
|
||||
var entries: [RDEPUBBookPageMapEntry] = []
|
||||
var absolutePageStart = 0
|
||||
for item in sorted {
|
||||
entries.append(RDEPUBBookPageMapEntry(
|
||||
spineIndex: item.spineIndex,
|
||||
href: item.href,
|
||||
title: item.title,
|
||||
pageCount: item.pageCount,
|
||||
absolutePageStart: absolutePageStart,
|
||||
fragmentOffsets: item.fragmentOffsets
|
||||
))
|
||||
absolutePageStart += item.pageCount
|
||||
}
|
||||
return RDEPUBBookPageMap(entries: entries)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user