2142 lines
73 KiB
Swift
2142 lines
73 KiB
Swift
//
|
||
// RDReaderController.swift
|
||
// RDReaderDemo
|
||
//
|
||
// Created by yangsq on 2021/8/10.
|
||
//
|
||
|
||
import SSAlertSwift
|
||
import UIKit
|
||
|
||
/// 书籍来源类型
|
||
public enum BookSource {
|
||
/// TXT文件(Bundle内置)
|
||
case textFile
|
||
/// EPUB文件
|
||
case epubFile(url: URL)
|
||
}
|
||
|
||
public class RDReaderController: UIViewController {
|
||
private static let epubPaginationCacheStorageKey = "epubPaginationCache.key"
|
||
private static let epubValidationStateStorageKey =
|
||
"ssreader.epub.validation.state"
|
||
|
||
public lazy var topToolView: RDReaderTopToolView = {
|
||
let view = RDReaderTopToolView(
|
||
frame: CGRect(
|
||
x: 0,
|
||
y: 0,
|
||
width: view.frame.width,
|
||
height: 44 + UIApplication.shared.statusBarFrame.height
|
||
)
|
||
)
|
||
return view
|
||
}()
|
||
|
||
public lazy var bottomToolView: RDReaderBottomToolView = {
|
||
let view = RDReaderBottomToolView(
|
||
frame: CGRect(
|
||
x: 0,
|
||
y: 0,
|
||
width: view.frame.width,
|
||
height: 55 + RDReaderCommon.safeAreaInsets.bottom
|
||
)
|
||
)
|
||
return view
|
||
}()
|
||
|
||
public lazy var readerView: RDReaderView = {
|
||
let readerView = RDReaderView()
|
||
return readerView
|
||
}()
|
||
|
||
// MARK: - TXT模式数据
|
||
var chapterPages = [ChapterPage]()
|
||
var book = Book(desc: nil, chapters: [])
|
||
var currentChapterNum: Int = 0
|
||
|
||
// MARK: - EPUB模式数据
|
||
var epubParser: RDEPUBParser?
|
||
var epubPublication: RDEPUBPublication?
|
||
var epubSession: RDEPUBReadingSession?
|
||
var epubPaginator: RDEPUBPaginator?
|
||
var epubReadingProfile: RDEPUBReadingProfile?
|
||
var epubTextBook: LegacyRDEPUBTextBook?
|
||
var epubTOCItems: [EPUBTOCDisplayItem] = []
|
||
var epubHighlights: [RDEPUBHighlight] = []
|
||
var currentEPUBBookIdentifier: String?
|
||
private var epubPreloadViews: [Int: RDReaderEPUBContentView] = [:]
|
||
private var epubPageCurlViews: [Int: RDReaderEPUBContentView] = [:]
|
||
private var isBackgroundEPUBPaginationRunning = false
|
||
private var backgroundEPUBPaginationApplyWorkItem: DispatchWorkItem?
|
||
private var epubValidationRunStartedAt: Date?
|
||
private var epubValidationRunReason = "initialLoad"
|
||
|
||
private var epubPages: [EPUBPage] {
|
||
epubSession?.activePages ?? []
|
||
}
|
||
|
||
private var isTextEPUBMode: Bool {
|
||
epubReadingProfile == .textReflowable
|
||
}
|
||
|
||
private var epubChapters: [EPUBChapterInfo] {
|
||
epubSession?.activeChapters ?? []
|
||
}
|
||
|
||
var epubResolver: RDEPUBResourceResolver? {
|
||
epubSession?.resourceResolver ?? epubPublication?.resourceResolver
|
||
}
|
||
|
||
func storeEPUBValidationState(_ values: [String: Any]) {
|
||
var state =
|
||
UserDefaults.standard.dictionary(
|
||
forKey: Self.epubValidationStateStorageKey
|
||
) ?? [:]
|
||
values.forEach { state[$0.key] = $0.value }
|
||
state["timestamp"] = ISO8601DateFormatter().string(from: Date())
|
||
UserDefaults.standard
|
||
.set(state, forKey: Self.epubValidationStateStorageKey)
|
||
}
|
||
|
||
func beginEPUBValidationRun(
|
||
reason: String,
|
||
restoreLocation: RDEPUBLocation?
|
||
) {
|
||
epubValidationRunStartedAt = Date()
|
||
epubValidationRunReason = reason
|
||
|
||
var state = epubValidationSettingsSnapshot()
|
||
state["validationReason"] = reason
|
||
state["restoreRequestedHref"] = restoreLocation?.href ?? ""
|
||
state["restoreRequestedFragment"] = restoreLocation?.fragment ?? ""
|
||
storeEPUBValidationState(state)
|
||
}
|
||
|
||
func storeEPUBPaginationValidation(
|
||
mode: String,
|
||
stage: String,
|
||
pageCount: Int,
|
||
chapterCount: Int,
|
||
tocCount: Int,
|
||
restoreLocation: RDEPUBLocation?,
|
||
resolvedRestorePage: Int?
|
||
) {
|
||
var state = epubValidationSettingsSnapshot()
|
||
state["mode"] = mode
|
||
state["validationReason"] = epubValidationRunReason
|
||
state["paginationStage"] = stage
|
||
state["pageCount"] = pageCount
|
||
state["chapterCount"] = chapterCount
|
||
state["tocCount"] = tocCount
|
||
state["restoreRequestedHref"] = restoreLocation?.href ?? ""
|
||
state["restoreRequestedFragment"] = restoreLocation?.fragment ?? ""
|
||
state["restoreResolvedPage"] = resolvedRestorePage ?? 0
|
||
if let startedAt = epubValidationRunStartedAt {
|
||
state["paginationDurationMs"] = Int(
|
||
Date().timeIntervalSince(startedAt) * 1000
|
||
)
|
||
}
|
||
storeEPUBValidationState(state)
|
||
}
|
||
|
||
func storeEPUBTOCValidation(item: EPUBTOCDisplayItem, resolvedPage: Int?) {
|
||
storeEPUBValidationState([
|
||
"tocSelectionTitle": item.title,
|
||
"tocSelectionHref": item.href,
|
||
"tocSelectionDepth": item.depth,
|
||
"tocSelectionResolvedPage": resolvedPage ?? 0,
|
||
"tocSelectionCount": epubTOCItems.count,
|
||
])
|
||
}
|
||
|
||
func storeEPUBEndValidationState(pageNum: Int, reachedEndScreen: Bool) {
|
||
storeEPUBValidationState([
|
||
"lastVisitedPage": pageNum,
|
||
"didReachReadableEnd": true,
|
||
"didReachEndScreen": reachedEndScreen,
|
||
])
|
||
}
|
||
|
||
private func epubValidationSettingsSnapshot() -> [String: Any] {
|
||
[
|
||
"settingsTheme": RDReaderManager.shared.themeType.currentType
|
||
.rawValue,
|
||
"settingsFontSize": RDReaderManager.shared.fontValue.currentType,
|
||
"settingsLineSpacing": RDReaderManager.shared.lineSpaceType
|
||
.currentType.rawValue,
|
||
"settingsLineSpacingMultiple": RDReaderManager.shared.lineSpaceType
|
||
.currentType.mulitiple,
|
||
"settingsDisplayType": RDReaderManager.shared.pageCurlTypeType
|
||
.currentType.rawValue,
|
||
"settingsBrightness": RDReaderManager.shared.brightValue
|
||
.currentType,
|
||
]
|
||
}
|
||
|
||
private func currentReadableEPUBPageCount() -> Int {
|
||
isTextEPUBMode ? (epubTextBook?.pages.count ?? 0) : epubPages.count
|
||
}
|
||
|
||
private func resolvedEPUBRestorePageNumber(for location: RDEPUBLocation?)
|
||
-> Int?
|
||
{
|
||
guard let location else { return nil }
|
||
return epubSession?
|
||
.pageIndex(for: location, bookIdentifier: currentEPUBBookIdentifier)
|
||
.map { $0 + 1 }
|
||
}
|
||
|
||
/// 加载指示器
|
||
private lazy var loadingIndicator: UIActivityIndicatorView = {
|
||
let indicator: UIActivityIndicatorView
|
||
if #available(iOS 13.0, *) {
|
||
indicator = UIActivityIndicatorView(style: .large)
|
||
} else {
|
||
indicator = UIActivityIndicatorView(style: .whiteLarge)
|
||
}
|
||
indicator.hidesWhenStopped = true
|
||
indicator.color = .gray
|
||
return indicator
|
||
}()
|
||
|
||
private lazy var epubPaginationHostView: UIView = {
|
||
let hostView = UIView(frame: .zero)
|
||
hostView.alpha = 0.01
|
||
hostView.isUserInteractionEnabled = false
|
||
hostView.clipsToBounds = true
|
||
hostView.accessibilityElementsHidden = true
|
||
return hostView
|
||
}()
|
||
|
||
private lazy var epubPreloadHostView: UIView = {
|
||
let hostView = UIView(frame: .zero)
|
||
hostView.alpha = 0.01
|
||
hostView.isUserInteractionEnabled = false
|
||
hostView.clipsToBounds = true
|
||
hostView.accessibilityElementsHidden = true
|
||
return hostView
|
||
}()
|
||
|
||
var isReaderContentReady = false
|
||
|
||
/// 书籍来源,默认为TXT文件
|
||
var bookSource: BookSource = .textFile
|
||
|
||
/// 便捷初始化,指定EPUB文件
|
||
convenience init(epubURL: URL) {
|
||
self.init()
|
||
self.bookSource = .epubFile(url: epubURL)
|
||
}
|
||
|
||
public override func viewDidLoad() {
|
||
super.viewDidLoad()
|
||
makeUI()
|
||
|
||
switch bookSource {
|
||
case .textFile:
|
||
loadData()
|
||
isReaderContentReady = true
|
||
readerView.reloadData()
|
||
transitionToPage()
|
||
case .epubFile(let url):
|
||
loadEPUBData(url: url)
|
||
}
|
||
}
|
||
|
||
public override func viewWillAppear(_ animated: Bool) {
|
||
super.viewWillAppear(animated)
|
||
self.navigationController?
|
||
.setNavigationBarHidden(true, animated: animated)
|
||
}
|
||
|
||
public override func viewWillDisappear(_ animated: Bool) {
|
||
super.viewWillDisappear(animated)
|
||
self.navigationController?
|
||
.setNavigationBarHidden(false, animated: animated)
|
||
}
|
||
|
||
// MARK: - TXT数据加载
|
||
|
||
func loadData() {
|
||
updateReaderViewConfiguration()
|
||
epubPublication = nil
|
||
epubSession = nil
|
||
epubReadingProfile = nil
|
||
epubTextBook = nil
|
||
currentEPUBBookIdentifier = nil
|
||
epubTOCItems = []
|
||
epubHighlights = []
|
||
clearStagedBackgroundEPUBPagination()
|
||
clearEPUBPreloadCache()
|
||
let fontSize = RDReaderManager.shared.fontValue.currentType
|
||
let lineSapce = RDReaderManager.shared.lineSpaceType.currentType
|
||
let pageWidth: CGFloat
|
||
if readerView.pagesPerScreen > 1 {
|
||
pageWidth =
|
||
view.frame.width
|
||
/ CGFloat(
|
||
readerView.pagesPerScreen
|
||
) - 16 * 2
|
||
} else {
|
||
pageWidth = view.frame.width - 16 * 2
|
||
}
|
||
let pageSize = CGSize(
|
||
width: pageWidth,
|
||
height: view.frame.height - 40 * 2
|
||
)
|
||
let font = UIFont.systemFont(ofSize: fontSize)
|
||
let lineSpaceValue = lineSapce.mulitiple * 15
|
||
|
||
book = RDReaderManager.shared
|
||
.encodeTextFile(
|
||
font: font,
|
||
lineSapce: lineSpaceValue,
|
||
size: pageSize
|
||
)
|
||
chapterPages = book.chapters.reduce(
|
||
[],
|
||
{ result, chapter in
|
||
result + chapter.pages
|
||
}
|
||
)
|
||
}
|
||
|
||
func transitionToPage(rangeLocation: Int? = nil) {
|
||
var rangeLoc = RDReaderManager.shared.readProgress.currentType
|
||
if rangeLocation != nil {
|
||
rangeLoc = rangeLocation!
|
||
}
|
||
if let pageNum = chapterPages.firstIndex(
|
||
where: { chapterPage in
|
||
let chapter = self.book.chapters[chapterPage.chapterSort - 1]
|
||
let chapterRange = chapter.range
|
||
let range = NSMakeRange(
|
||
chapterRange.location + chapterPage.range.location,
|
||
chapterPage.range.length
|
||
)
|
||
return range.contains(rangeLoc)
|
||
})
|
||
{
|
||
readerView.transitionToPage(pageNum: pageNum + 1)
|
||
}
|
||
}
|
||
|
||
// MARK: - EPUB数据加载(异步)
|
||
|
||
func loadEPUBData(url: URL) {
|
||
isReaderContentReady = false
|
||
currentEPUBBookIdentifier = nil
|
||
epubTOCItems = []
|
||
epubHighlights = []
|
||
clearStagedBackgroundEPUBPagination()
|
||
clearEPUBPreloadCache()
|
||
// 显示加载指示器
|
||
showLoading()
|
||
|
||
// 1. 解析EPUB结构
|
||
let parser = RDEPUBParser()
|
||
do {
|
||
try parser.parse(epubURL: url)
|
||
} catch {
|
||
hideLoading()
|
||
print("EPUB解析失败: \(error.localizedDescription)")
|
||
return
|
||
}
|
||
self.epubParser = parser
|
||
let publication = parser.makePublication()
|
||
self.epubPublication = publication
|
||
let session = RDEPUBReadingSession(publication: publication)
|
||
session.transition(to: .loading)
|
||
self.epubSession = session
|
||
self.epubReadingProfile = publication.readingProfile
|
||
self.epubTextBook = nil
|
||
updateReaderViewConfiguration()
|
||
self.currentEPUBBookIdentifier =
|
||
parser.metadata.identifier ?? url.lastPathComponent
|
||
self.epubHighlights = storedEPUBHighlights()
|
||
let savedLocation = storedEPUBLocation()
|
||
beginEPUBValidationRun(
|
||
reason: "initialLoad",
|
||
restoreLocation: savedLocation
|
||
)
|
||
|
||
let debugBookName =
|
||
title ?? url.deletingPathExtension().lastPathComponent
|
||
print(
|
||
"[EPUB] Load book=\(debugBookName) profile=\(publication.readingProfile.rawValue) spine=\(parser.spine.count)"
|
||
)
|
||
storeEPUBValidationState([
|
||
"bookTitle": debugBookName,
|
||
"profile": publication.readingProfile.rawValue,
|
||
"spineCount": parser.spine.count,
|
||
"bookIdentifier": currentEPUBBookIdentifier ?? "",
|
||
])
|
||
|
||
if publication.readingProfile == .textReflowable {
|
||
loadEPUBTextBook(
|
||
parser: parser,
|
||
publication: publication,
|
||
restoreLocation: savedLocation
|
||
)
|
||
return
|
||
}
|
||
|
||
// 2. 使用WKWebView预计算每章页数
|
||
let layoutContext = currentEPUBLayoutContext()
|
||
let viewportSize = layoutContext.viewportSize
|
||
let preferences = RDReaderManager.shared.epubPreferences(
|
||
reflowableInsets: layoutContext.reflowableContentInsets,
|
||
fixedContentInset: layoutContext.fixedContentInset
|
||
)
|
||
if let cacheKey = epubPaginationCacheKey(
|
||
parser: parser,
|
||
viewportSize: viewportSize,
|
||
padding: preferences.reflowableContentInsets,
|
||
fontSize: preferences.fontSize,
|
||
lineHeightMultiple: preferences.lineHeightMultiple
|
||
),
|
||
let cachedPageCounts = cachedEPUBPageCounts(
|
||
for: cacheKey,
|
||
expectedSpineCount: parser.spine.count
|
||
)
|
||
{
|
||
finishEPUBPagination(
|
||
pageCounts: cachedPageCounts,
|
||
restoreLocation: savedLocation,
|
||
validationStage: "cache"
|
||
)
|
||
return
|
||
}
|
||
|
||
if parser.metadata.layout != .fixed {
|
||
let initialSpineIndex = initialEPUBSpineIndex(
|
||
for: savedLocation,
|
||
parser: parser
|
||
)
|
||
let paginator = RDEPUBPaginator()
|
||
self.epubPaginator = paginator
|
||
paginator.calculateSingleSpinePageCount(
|
||
parser: parser,
|
||
spineIndex: initialSpineIndex,
|
||
hostingView: ensureEPUBPaginationHostView(
|
||
viewportSize: viewportSize
|
||
),
|
||
presentation:
|
||
preferences
|
||
.presentationStyle(viewportSize: viewportSize)
|
||
) { [weak self] pageCount in
|
||
guard let self = self else { return }
|
||
var provisionalPageCounts = Array(
|
||
repeating: 1,
|
||
count: parser.spine.count
|
||
)
|
||
provisionalPageCounts[initialSpineIndex] = max(1, pageCount)
|
||
self.finishEPUBPagination(
|
||
pageCounts: provisionalPageCounts,
|
||
restoreLocation: savedLocation,
|
||
validationStage: "provisional"
|
||
)
|
||
self.epubPaginator = nil
|
||
self.startBackgroundEPUBPagination(
|
||
parser: parser,
|
||
viewportSize: viewportSize,
|
||
padding: preferences.reflowableContentInsets,
|
||
fontSize: preferences.fontSize,
|
||
lineHeightMultiple: preferences.lineHeightMultiple,
|
||
restoreLocation: savedLocation
|
||
)
|
||
}
|
||
return
|
||
}
|
||
|
||
let paginator = RDEPUBPaginator()
|
||
self.epubPaginator = paginator
|
||
|
||
paginator.calculate(
|
||
parser: parser,
|
||
hostingView: ensureEPUBPaginationHostView(
|
||
viewportSize: viewportSize
|
||
),
|
||
presentation:
|
||
preferences
|
||
.presentationStyle(viewportSize: viewportSize)
|
||
) { [weak self] pageCounts in
|
||
guard let self = self else { return }
|
||
if let cacheKey = self.epubPaginationCacheKey(
|
||
parser: parser,
|
||
viewportSize: viewportSize,
|
||
padding: preferences.reflowableContentInsets,
|
||
fontSize: preferences.fontSize,
|
||
lineHeightMultiple: preferences.lineHeightMultiple
|
||
) {
|
||
self.saveEPUBPageCounts(pageCounts, for: cacheKey)
|
||
}
|
||
self.finishEPUBPagination(
|
||
pageCounts: pageCounts,
|
||
restoreLocation: savedLocation,
|
||
validationStage: "initial"
|
||
)
|
||
self.epubPaginator = nil
|
||
}
|
||
}
|
||
|
||
/// 重新分页EPUB(字号/行距改变时)
|
||
func repaginateEPUB(reason: String = "repaginate") {
|
||
guard let parser = epubParser, let session = epubSession else { return }
|
||
updateReaderViewConfiguration()
|
||
session.transition(to: .repaginating)
|
||
|
||
isReaderContentReady = false
|
||
showLoading()
|
||
clearStagedBackgroundEPUBPagination()
|
||
clearEPUBPreloadCache()
|
||
|
||
let layoutContext = currentEPUBLayoutContext()
|
||
let viewportSize = layoutContext.viewportSize
|
||
let preferences = RDReaderManager.shared.epubPreferences(
|
||
reflowableInsets: layoutContext.reflowableContentInsets,
|
||
fixedContentInset: layoutContext.fixedContentInset
|
||
)
|
||
|
||
let savedLocation = storedEPUBLocation() ?? currentVisibleEPUBLocation()
|
||
beginEPUBValidationRun(reason: reason, restoreLocation: savedLocation)
|
||
if isTextEPUBMode, let publication = epubPublication {
|
||
loadEPUBTextBook(
|
||
parser: parser,
|
||
publication: publication,
|
||
restoreLocation: savedLocation
|
||
)
|
||
return
|
||
}
|
||
|
||
if let cacheKey = epubPaginationCacheKey(
|
||
parser: parser,
|
||
viewportSize: viewportSize,
|
||
padding: preferences.reflowableContentInsets,
|
||
fontSize: preferences.fontSize,
|
||
lineHeightMultiple: preferences.lineHeightMultiple
|
||
),
|
||
let cachedPageCounts = cachedEPUBPageCounts(
|
||
for: cacheKey,
|
||
expectedSpineCount: parser.spine.count
|
||
)
|
||
{
|
||
finishEPUBPagination(
|
||
pageCounts: cachedPageCounts,
|
||
restoreLocation: savedLocation,
|
||
validationStage: "cache"
|
||
)
|
||
return
|
||
}
|
||
|
||
if parser.metadata.layout != .fixed {
|
||
let initialSpineIndex = initialEPUBSpineIndex(
|
||
for: savedLocation,
|
||
parser: parser
|
||
)
|
||
let paginator = RDEPUBPaginator()
|
||
self.epubPaginator = paginator
|
||
paginator.calculateSingleSpinePageCount(
|
||
parser: parser,
|
||
spineIndex: initialSpineIndex,
|
||
hostingView: ensureEPUBPaginationHostView(
|
||
viewportSize: viewportSize
|
||
),
|
||
presentation:
|
||
preferences
|
||
.presentationStyle(viewportSize: viewportSize)
|
||
) { [weak self] pageCount in
|
||
guard let self = self else { return }
|
||
var provisionalPageCounts = Array(
|
||
repeating: 1,
|
||
count: parser.spine.count
|
||
)
|
||
provisionalPageCounts[initialSpineIndex] = max(1, pageCount)
|
||
self.finishEPUBPagination(
|
||
pageCounts: provisionalPageCounts,
|
||
restoreLocation: savedLocation,
|
||
validationStage: "provisional"
|
||
)
|
||
self.epubPaginator = nil
|
||
self.startBackgroundEPUBPagination(
|
||
parser: parser,
|
||
viewportSize: viewportSize,
|
||
padding: preferences.reflowableContentInsets,
|
||
fontSize: preferences.fontSize,
|
||
lineHeightMultiple: preferences.lineHeightMultiple,
|
||
restoreLocation: savedLocation
|
||
)
|
||
}
|
||
return
|
||
}
|
||
|
||
let paginator = RDEPUBPaginator()
|
||
self.epubPaginator = paginator
|
||
|
||
paginator.calculate(
|
||
parser: parser,
|
||
hostingView: ensureEPUBPaginationHostView(
|
||
viewportSize: viewportSize
|
||
),
|
||
presentation:
|
||
preferences
|
||
.presentationStyle(viewportSize: viewportSize)
|
||
) { [weak self] pageCounts in
|
||
guard let self = self else { return }
|
||
if let cacheKey = self.epubPaginationCacheKey(
|
||
parser: parser,
|
||
viewportSize: viewportSize,
|
||
padding: preferences.reflowableContentInsets,
|
||
fontSize: preferences.fontSize,
|
||
lineHeightMultiple: preferences.lineHeightMultiple
|
||
) {
|
||
self.saveEPUBPageCounts(pageCounts, for: cacheKey)
|
||
}
|
||
self.finishEPUBPagination(
|
||
pageCounts: pageCounts,
|
||
restoreLocation: savedLocation,
|
||
validationStage: "initial"
|
||
)
|
||
self.epubPaginator = nil
|
||
}
|
||
}
|
||
|
||
private func finishEPUBPagination(
|
||
pageCounts: [Int],
|
||
restoreLocation: RDEPUBLocation?,
|
||
validationStage: String
|
||
) {
|
||
guard let session = epubSession else { return }
|
||
let snapshot = session.makePaginationSnapshot(
|
||
pageCounts: pageCounts,
|
||
preferences: currentEPUBPreferences(),
|
||
layoutContext: currentEPUBLayoutContext()
|
||
)
|
||
applyEPUBPaginationSnapshot(
|
||
snapshot,
|
||
restoreLocation: restoreLocation,
|
||
hideLoadingIndicator: true,
|
||
validationStage: validationStage
|
||
)
|
||
}
|
||
|
||
private func applyEPUBPaginationSnapshot(
|
||
_ snapshot: (pages: [EPUBPage], chapters: [EPUBChapterInfo]),
|
||
restoreLocation: RDEPUBLocation?,
|
||
hideLoadingIndicator: Bool,
|
||
validationStage: String
|
||
) {
|
||
epubSession?.setActiveSnapshot(snapshot)
|
||
if let publication = epubPublication {
|
||
epubTOCItems = flattenedTOCItems(from: publication.tableOfContents)
|
||
}
|
||
storeEPUBPaginationValidation(
|
||
mode: epubReadingProfile?.rawValue ?? "unknown",
|
||
stage: validationStage,
|
||
pageCount: snapshot.pages.count,
|
||
chapterCount: snapshot.chapters.count,
|
||
tocCount: epubTOCItems.count,
|
||
restoreLocation: restoreLocation,
|
||
resolvedRestorePage: resolvedEPUBRestorePageNumber(
|
||
for: restoreLocation
|
||
)
|
||
)
|
||
isReaderContentReady = true
|
||
if hideLoadingIndicator {
|
||
hideLoading()
|
||
}
|
||
readerView.reloadData()
|
||
restoreEPUBLocation(restoreLocation)
|
||
if restoreLocation == nil {
|
||
epubSession?.transition(to: .idle)
|
||
}
|
||
}
|
||
|
||
private func queueBackgroundEPUBPaginationApplication(
|
||
pageCounts: [Int],
|
||
restoreLocation: RDEPUBLocation?
|
||
) {
|
||
guard let session = epubSession else { return }
|
||
let snapshot = session.makePaginationSnapshot(
|
||
pageCounts: pageCounts,
|
||
preferences: currentEPUBPreferences(),
|
||
layoutContext: currentEPUBLayoutContext()
|
||
)
|
||
epubSession?.stageSnapshot(snapshot, restoreLocation: restoreLocation)
|
||
schedulePendingBackgroundEPUBPaginationApplication()
|
||
}
|
||
|
||
private func schedulePendingBackgroundEPUBPaginationApplication() {
|
||
backgroundEPUBPaginationApplyWorkItem?.cancel()
|
||
let workItem = DispatchWorkItem { [weak self] in
|
||
self?.applyPendingBackgroundEPUBPaginationIfNeeded()
|
||
}
|
||
backgroundEPUBPaginationApplyWorkItem = workItem
|
||
DispatchQueue.main
|
||
.asyncAfter(deadline: .now() + 0.35, execute: workItem)
|
||
}
|
||
|
||
private func applyPendingBackgroundEPUBPaginationIfNeeded() {
|
||
if let staged = epubSession?.consumeStagedSnapshotIfAllowed() {
|
||
let restoreLocation =
|
||
staged.restoreLocation ?? storedEPUBLocation()
|
||
?? currentVisibleEPUBLocation()
|
||
backgroundEPUBPaginationApplyWorkItem = nil
|
||
applyEPUBPaginationSnapshot(
|
||
staged.snapshot,
|
||
restoreLocation: restoreLocation,
|
||
hideLoadingIndicator: false,
|
||
validationStage: "backgroundFinal"
|
||
)
|
||
return
|
||
}
|
||
guard epubSession?.stagedSnapshot() != nil else { return }
|
||
schedulePendingBackgroundEPUBPaginationApplication()
|
||
}
|
||
|
||
private func clearStagedBackgroundEPUBPagination() {
|
||
backgroundEPUBPaginationApplyWorkItem?.cancel()
|
||
backgroundEPUBPaginationApplyWorkItem = nil
|
||
epubSession?.clearStagedSnapshot()
|
||
}
|
||
|
||
private func initialEPUBSpineIndex(
|
||
for location: RDEPUBLocation?,
|
||
parser: RDEPUBParser
|
||
) -> Int {
|
||
let _ = parser
|
||
return epubSession?.initialSpineIndex(for: location) ?? 0
|
||
}
|
||
|
||
private func startBackgroundEPUBPagination(
|
||
parser: RDEPUBParser,
|
||
viewportSize: CGSize,
|
||
padding: UIEdgeInsets,
|
||
fontSize: CGFloat,
|
||
lineHeightMultiple: CGFloat,
|
||
restoreLocation: RDEPUBLocation?
|
||
) {
|
||
guard !isBackgroundEPUBPaginationRunning else { return }
|
||
isBackgroundEPUBPaginationRunning = true
|
||
|
||
let paginator = RDEPUBPaginator()
|
||
self.epubPaginator = paginator
|
||
let preferences = currentEPUBPreferences()
|
||
paginator.calculate(
|
||
parser: parser,
|
||
hostingView: ensureEPUBPaginationHostView(
|
||
viewportSize: viewportSize
|
||
),
|
||
presentation:
|
||
preferences
|
||
.presentationStyle(viewportSize: viewportSize)
|
||
) { [weak self] pageCounts in
|
||
guard let self = self else { return }
|
||
self.isBackgroundEPUBPaginationRunning = false
|
||
if let cacheKey = self.epubPaginationCacheKey(
|
||
parser: parser,
|
||
viewportSize: viewportSize,
|
||
padding: padding,
|
||
fontSize: fontSize,
|
||
lineHeightMultiple: lineHeightMultiple
|
||
) {
|
||
self.saveEPUBPageCounts(pageCounts, for: cacheKey)
|
||
}
|
||
let currentLocation =
|
||
self.storedEPUBLocation() ?? self.currentVisibleEPUBLocation()
|
||
?? restoreLocation
|
||
self.queueBackgroundEPUBPaginationApplication(
|
||
pageCounts: pageCounts,
|
||
restoreLocation: currentLocation
|
||
)
|
||
self.epubPaginator = nil
|
||
}
|
||
}
|
||
|
||
private func epubPaginationCacheKey(
|
||
parser: RDEPUBParser,
|
||
viewportSize: CGSize,
|
||
padding: UIEdgeInsets,
|
||
fontSize: CGFloat,
|
||
lineHeightMultiple: CGFloat
|
||
) -> String? {
|
||
let bookSignature: String
|
||
switch bookSource {
|
||
case .textFile:
|
||
return nil
|
||
case .epubFile(let url):
|
||
let attributes = try? FileManager.default.attributesOfItem(
|
||
atPath: url.path
|
||
)
|
||
let fileSize = (attributes?[.size] as? NSNumber)?.stringValue ?? "0"
|
||
let modifiedAt =
|
||
(attributes?[.modificationDate] as? Date)?.timeIntervalSince1970
|
||
?? 0
|
||
bookSignature = [
|
||
url.lastPathComponent,
|
||
fileSize,
|
||
String(format: "%.0f", modifiedAt),
|
||
]
|
||
.joined(separator: "|")
|
||
}
|
||
|
||
let layoutSignature = [
|
||
Int(viewportSize.width.rounded()).description,
|
||
Int(viewportSize.height.rounded()).description,
|
||
Int(padding.top.rounded()).description,
|
||
Int(padding.left.rounded()).description,
|
||
Int(padding.bottom.rounded()).description,
|
||
Int(padding.right.rounded()).description,
|
||
String(format: "%.2f", fontSize),
|
||
String(format: "%.2f", lineHeightMultiple),
|
||
String(readerView.pagesPerScreen),
|
||
parser.metadata.layout.rawValue,
|
||
String(parser.spine.count),
|
||
].joined(separator: "|")
|
||
|
||
return bookSignature + "|" + layoutSignature
|
||
}
|
||
|
||
private func cachedEPUBPageCounts(for key: String, expectedSpineCount: Int)
|
||
-> [Int]?
|
||
{
|
||
let cache =
|
||
UserDefaults.standard.dictionary(
|
||
forKey: Self.epubPaginationCacheStorageKey
|
||
) ?? [:]
|
||
let rawCounts = cache[key]
|
||
let counts: [Int]?
|
||
if let values = rawCounts as? [Int] {
|
||
counts = values
|
||
} else if let values = rawCounts as? [NSNumber] {
|
||
counts = values.map(\.intValue)
|
||
} else {
|
||
counts = nil
|
||
}
|
||
guard let counts, counts.count == expectedSpineCount else {
|
||
return nil
|
||
}
|
||
return counts.map { max(1, $0) }
|
||
}
|
||
|
||
private func saveEPUBPageCounts(_ pageCounts: [Int], for key: String) {
|
||
var cache =
|
||
UserDefaults.standard.dictionary(
|
||
forKey: Self.epubPaginationCacheStorageKey
|
||
) ?? [:]
|
||
cache[key] = pageCounts.map { max(1, $0) }
|
||
UserDefaults.standard
|
||
.set(cache, forKey: Self.epubPaginationCacheStorageKey)
|
||
}
|
||
|
||
private func currentEPUBLayoutContext() -> RDEPUBNavigatorLayoutContext {
|
||
let readerBounds =
|
||
readerView.bounds.size == .zero
|
||
? view.bounds.size : readerView.bounds.size
|
||
let safeAreaInsets = view.window?.safeAreaInsets ?? view.safeAreaInsets
|
||
return RDEPUBNavigatorLayoutContext(
|
||
containerSize: readerBounds,
|
||
pagesPerScreen: readerView.pagesPerScreen,
|
||
safeAreaInsets: safeAreaInsets,
|
||
userInterfaceIdiom: UIDevice.current.userInterfaceIdiom
|
||
)
|
||
}
|
||
|
||
private func currentEPUBPreferences(theme: RDReaderTheme? = nil)
|
||
-> RDEPUBPreferences
|
||
{
|
||
let layoutContext = currentEPUBLayoutContext()
|
||
return RDReaderManager.shared.epubPreferences(
|
||
theme: theme,
|
||
reflowableInsets: layoutContext.reflowableContentInsets,
|
||
fixedContentInset: layoutContext.fixedContentInset
|
||
)
|
||
}
|
||
|
||
private func renderRequest(
|
||
for pageNum: Int,
|
||
page: EPUBPage,
|
||
publication: RDEPUBPublication,
|
||
theme: RDReaderTheme
|
||
) -> RDEPUBRenderRequest? {
|
||
let preferences = currentEPUBPreferences(theme: theme)
|
||
let layoutContext = currentEPUBLayoutContext()
|
||
let targetLocation = epubSession?.pendingLocation(
|
||
forPageNumber: pageNum,
|
||
spineIndex: page.fixedSpread == nil ? page.spineIndex : nil
|
||
)
|
||
return preferences.renderRequest(
|
||
for: page,
|
||
publication: publication,
|
||
viewportSize: layoutContext.viewportSize,
|
||
targetLocation: targetLocation,
|
||
highlights: highlights(for: page.spineIndex)
|
||
)
|
||
}
|
||
|
||
private func updateReaderViewConfiguration() {
|
||
switch bookSource {
|
||
case .textFile:
|
||
readerView.landscapeDualPageEnabled = true
|
||
readerView.pageDirection = .rightToLeft
|
||
case .epubFile:
|
||
let isFixedLayout = epubPublication?.layout == .fixed
|
||
readerView.landscapeDualPageEnabled = !isFixedLayout
|
||
|
||
switch epubPublication?.readingProgression {
|
||
case .ltr:
|
||
readerView.pageDirection = .leftToRight
|
||
case .rtl:
|
||
readerView.pageDirection = .rightToLeft
|
||
case .some(.auto):
|
||
readerView.pageDirection = .rightToLeft
|
||
case .none:
|
||
readerView.pageDirection = .rightToLeft
|
||
}
|
||
|
||
let preferredDisplayType = effectiveDisplayType(
|
||
requested: RDReaderManager.shared.pageCurlTypeType.currentType
|
||
.value
|
||
)
|
||
if readerView.currentDisplayType != preferredDisplayType {
|
||
readerView.switchReaderDisplayType(preferredDisplayType)
|
||
}
|
||
}
|
||
}
|
||
|
||
private func effectiveDisplayType(requested: RDReaderView.DisplayType)
|
||
-> RDReaderView.DisplayType
|
||
{
|
||
guard epubPublication?.layout == .fixed else {
|
||
return requested
|
||
}
|
||
|
||
if requested == .pageCurl {
|
||
return .horizontalScroll
|
||
}
|
||
return requested
|
||
}
|
||
|
||
private func saveEPUBLocation(_ location: RDEPUBLocation) {
|
||
let scopedLocation = RDEPUBLocation(
|
||
bookIdentifier: currentEPUBBookIdentifier,
|
||
href: location.href,
|
||
progression: location.progression,
|
||
lastProgression: location.lastProgression,
|
||
fragment: location.fragment
|
||
)
|
||
guard
|
||
let data = try? JSONEncoder().encode(scopedLocation),
|
||
let json = String(data: data, encoding: .utf8)
|
||
else {
|
||
return
|
||
}
|
||
RDReaderManager.shared.epubReadLocation.currentType = json
|
||
}
|
||
|
||
private func saveEPUBSelection(_ selection: RDEPUBSelection?) {
|
||
guard let selection = selection, !selection.isEmpty else {
|
||
RDReaderManager.shared.epubSelection.currentType = ""
|
||
return
|
||
}
|
||
let scopedSelection = RDEPUBSelection(
|
||
bookIdentifier: currentEPUBBookIdentifier,
|
||
location: RDEPUBLocation(
|
||
bookIdentifier: currentEPUBBookIdentifier,
|
||
href: selection.location.href,
|
||
progression: selection.location.progression,
|
||
lastProgression: selection.location.lastProgression,
|
||
fragment: selection.location.fragment
|
||
),
|
||
text: selection.text,
|
||
rangeInfo: selection.rangeInfo,
|
||
createdAt: selection.createdAt
|
||
)
|
||
guard
|
||
let data = try? JSONEncoder().encode(scopedSelection),
|
||
let json = String(data: data, encoding: .utf8)
|
||
else {
|
||
return
|
||
}
|
||
RDReaderManager.shared.epubSelection.currentType = json
|
||
}
|
||
|
||
private func storedEPUBSelection() -> RDEPUBSelection? {
|
||
let json = RDReaderManager.shared.epubSelection.currentType
|
||
guard !json.isEmpty, let data = json.data(using: .utf8) else {
|
||
return nil
|
||
}
|
||
guard
|
||
let selection = try? JSONDecoder().decode(
|
||
RDEPUBSelection.self,
|
||
from: data
|
||
)
|
||
else {
|
||
return nil
|
||
}
|
||
guard selection.bookIdentifier == currentEPUBBookIdentifier else {
|
||
return nil
|
||
}
|
||
return selection
|
||
}
|
||
|
||
private func allStoredEPUBHighlights() -> [RDEPUBHighlight] {
|
||
let json = RDReaderManager.shared.epubHighlights.currentType
|
||
guard !json.isEmpty, let data = json.data(using: .utf8) else {
|
||
return []
|
||
}
|
||
return (try? JSONDecoder().decode([RDEPUBHighlight].self, from: data))
|
||
?? []
|
||
}
|
||
|
||
private func storedEPUBHighlights() -> [RDEPUBHighlight] {
|
||
allStoredEPUBHighlights()
|
||
.filter { $0.bookIdentifier == currentEPUBBookIdentifier }
|
||
}
|
||
|
||
private func saveEPUBHighlights(_ highlights: [RDEPUBHighlight]) {
|
||
let scopedHighlights = highlights.map { highlight in
|
||
RDEPUBHighlight(
|
||
id: highlight.id,
|
||
bookIdentifier: currentEPUBBookIdentifier,
|
||
location: RDEPUBLocation(
|
||
bookIdentifier: currentEPUBBookIdentifier,
|
||
href: highlight.location.href,
|
||
progression: highlight.location.progression,
|
||
lastProgression: highlight.location.lastProgression,
|
||
fragment: highlight.location.fragment
|
||
),
|
||
text: highlight.text,
|
||
rangeInfo: highlight.rangeInfo,
|
||
color: highlight.color,
|
||
note: highlight.note,
|
||
createdAt: highlight.createdAt
|
||
)
|
||
}
|
||
let otherBookHighlights = allStoredEPUBHighlights().filter {
|
||
$0.bookIdentifier != currentEPUBBookIdentifier
|
||
}
|
||
let mergedHighlights = otherBookHighlights + scopedHighlights
|
||
guard let data = try? JSONEncoder().encode(mergedHighlights),
|
||
let json = String(data: data, encoding: .utf8)
|
||
else {
|
||
return
|
||
}
|
||
RDReaderManager.shared.epubHighlights.currentType = json
|
||
}
|
||
|
||
private func addHighlightFromCurrentSelection() {
|
||
guard let selection = storedEPUBSelection(),
|
||
let normalizedLocation = normalizedEPUBLocation(
|
||
selection.location
|
||
),
|
||
!selection.isEmpty
|
||
else {
|
||
return
|
||
}
|
||
|
||
let newHighlight = RDEPUBHighlight(
|
||
bookIdentifier: currentEPUBBookIdentifier,
|
||
location: normalizedLocation,
|
||
text: selection.text,
|
||
rangeInfo: selection.rangeInfo
|
||
)
|
||
var highlights = storedEPUBHighlights()
|
||
let isDuplicate = highlights.contains { highlight in
|
||
highlight.location.href == newHighlight.location.href
|
||
&& highlight.location.fragment == newHighlight.location.fragment
|
||
&& highlight.text == newHighlight.text
|
||
&& highlight.rangeInfo == newHighlight.rangeInfo
|
||
}
|
||
guard !isDuplicate else {
|
||
return
|
||
}
|
||
|
||
highlights.append(newHighlight)
|
||
epubHighlights = highlights
|
||
saveEPUBHighlights(highlights)
|
||
saveEPUBSelection(nil)
|
||
|
||
let currentLocation = currentVisibleEPUBLocation()
|
||
if isReaderContentReady {
|
||
readerView.reloadData()
|
||
restoreEPUBLocation(currentLocation)
|
||
}
|
||
}
|
||
|
||
private func storedEPUBLocation() -> RDEPUBLocation? {
|
||
let json = RDReaderManager.shared.epubReadLocation.currentType
|
||
guard !json.isEmpty, let data = json.data(using: .utf8) else {
|
||
return nil
|
||
}
|
||
guard
|
||
let location = try? JSONDecoder().decode(
|
||
RDEPUBLocation.self,
|
||
from: data
|
||
)
|
||
else {
|
||
return nil
|
||
}
|
||
guard location.bookIdentifier == currentEPUBBookIdentifier else {
|
||
return nil
|
||
}
|
||
return location
|
||
}
|
||
|
||
private func currentVisibleEPUBLocation() -> RDEPUBLocation? {
|
||
if isTextEPUBMode {
|
||
return epubTextBook?
|
||
.location(
|
||
forPageNumber: readerView.currentPage,
|
||
bookIdentifier: currentEPUBBookIdentifier
|
||
)
|
||
}
|
||
if let location = epubSession?.currentReadingContext?.location {
|
||
return location
|
||
}
|
||
return epubSession?.currentVisibleLocation(
|
||
currentPageNumber: readerView.currentPage,
|
||
bookIdentifier: currentEPUBBookIdentifier
|
||
)
|
||
}
|
||
|
||
private func pageContains(spineIndex: Int, in page: EPUBPage) -> Bool {
|
||
if let fixedSpread = page.fixedSpread {
|
||
return fixedSpread.resources
|
||
.contains(where: { $0.spineIndex == spineIndex })
|
||
}
|
||
return page.spineIndex == spineIndex
|
||
}
|
||
|
||
private func fallbackEPUBLocation(for page: EPUBPage) -> RDEPUBLocation? {
|
||
epubSession?
|
||
.fallbackLocation(
|
||
for: page,
|
||
bookIdentifier: currentEPUBBookIdentifier
|
||
)
|
||
}
|
||
|
||
private func normalizedEPUBHref(
|
||
_ href: String,
|
||
relativeTo spineIndex: Int? = nil
|
||
) -> String? {
|
||
epubResolver?.normalizedHref(href, relativeToSpineIndex: spineIndex)
|
||
?? href.components(separatedBy: "#").first
|
||
}
|
||
|
||
private func normalizedEPUBLocation(
|
||
_ location: RDEPUBLocation,
|
||
relativeTo spineIndex: Int? = nil
|
||
) -> RDEPUBLocation? {
|
||
epubResolver?.normalizedLocation(
|
||
location,
|
||
relativeToSpineIndex: spineIndex,
|
||
bookIdentifier: currentEPUBBookIdentifier
|
||
)
|
||
}
|
||
|
||
private func currentEPUBTOCIndex() -> Int {
|
||
guard !epubTOCItems.isEmpty else {
|
||
return currentChapterNum
|
||
}
|
||
if isTextEPUBMode {
|
||
let currentPageNumber = readerView.currentPage
|
||
guard currentPageNumber > 0 else {
|
||
return 0
|
||
}
|
||
return
|
||
epubTOCItems
|
||
.lastIndex(
|
||
where: {
|
||
($0.pageNumber ?? Int.max) <= currentPageNumber
|
||
}) ?? 0
|
||
}
|
||
let referenceLocation =
|
||
storedEPUBLocation() ?? currentVisibleEPUBLocation()
|
||
guard let location = referenceLocation,
|
||
let normalizedLocation = normalizedEPUBLocation(location)
|
||
else {
|
||
return 0
|
||
}
|
||
if let exactIndex = epubTOCItems.firstIndex(where: { item in
|
||
guard
|
||
let normalizedItem = normalizedEPUBLocation(
|
||
RDEPUBLocation(
|
||
bookIdentifier: currentEPUBBookIdentifier,
|
||
href: item.href,
|
||
progression: 0
|
||
)
|
||
)
|
||
else {
|
||
return false
|
||
}
|
||
return normalizedItem.href == normalizedLocation.href
|
||
&& normalizedItem.fragment == normalizedLocation.fragment
|
||
}) {
|
||
return exactIndex
|
||
}
|
||
if let pathIndex = epubTOCItems.lastIndex(where: { item in
|
||
guard
|
||
let normalizedItem = normalizedEPUBLocation(
|
||
RDEPUBLocation(
|
||
bookIdentifier: currentEPUBBookIdentifier,
|
||
href: item.href,
|
||
progression: 0
|
||
)
|
||
)
|
||
else {
|
||
return false
|
||
}
|
||
return normalizedItem.href == normalizedLocation.href
|
||
}) {
|
||
return pathIndex
|
||
}
|
||
return 0
|
||
}
|
||
|
||
private func pageIndex(for location: RDEPUBLocation) -> Int? {
|
||
epubSession?
|
||
.pageIndex(for: location, bookIdentifier: currentEPUBBookIdentifier)
|
||
}
|
||
|
||
private func highlights(for spineIndex: Int) -> [RDEPUBHighlight] {
|
||
guard let parser = epubParser,
|
||
parser.spine.indices
|
||
.contains(spineIndex)
|
||
else {
|
||
return []
|
||
}
|
||
let targetHref = normalizedEPUBHref(parser.spine[spineIndex].href)
|
||
return epubHighlights.filter { highlight in
|
||
normalizedEPUBHref(highlight.location.href) == targetHref
|
||
}
|
||
}
|
||
|
||
private func restoreEPUBLocationIfNeeded() {
|
||
restoreEPUBLocation(storedEPUBLocation())
|
||
}
|
||
|
||
func restoreEPUBLocation(_ location: RDEPUBLocation?) {
|
||
if isTextEPUBMode {
|
||
guard let location,
|
||
let targetPageNum = epubTextBook?.pageNumber(
|
||
for: location,
|
||
resolver: epubResolver ?? publicationFallbackResolver(),
|
||
bookIdentifier: currentEPUBBookIdentifier
|
||
)
|
||
else {
|
||
epubSession?.transition(to: .idle)
|
||
return
|
||
}
|
||
readerView.transitionToPage(pageNum: targetPageNum)
|
||
return
|
||
}
|
||
|
||
guard let session = epubSession,
|
||
let location = location,
|
||
let targetPageNum = session.queueNavigation(
|
||
to: location,
|
||
relativeToSpineIndex: nil,
|
||
bookIdentifier: currentEPUBBookIdentifier
|
||
)
|
||
else {
|
||
epubSession?.transition(to: .idle)
|
||
return
|
||
}
|
||
readerView.transitionToPage(pageNum: targetPageNum)
|
||
}
|
||
|
||
private func navigateToEPUBLink(
|
||
_ location: RDEPUBLocation,
|
||
fromSpineIndex spineIndex: Int
|
||
) {
|
||
guard let session = epubSession,
|
||
let targetPageNum = session.queueNavigation(
|
||
to: location,
|
||
relativeToSpineIndex: spineIndex,
|
||
bookIdentifier: currentEPUBBookIdentifier
|
||
)
|
||
else {
|
||
return
|
||
}
|
||
readerView.transitionToPage(pageNum: targetPageNum)
|
||
}
|
||
|
||
// MARK: - Loading Indicator
|
||
|
||
private func showLoading() {
|
||
view.addSubview(loadingIndicator)
|
||
loadingIndicator.center = view.center
|
||
loadingIndicator.startAnimating()
|
||
}
|
||
|
||
func hideLoading() {
|
||
loadingIndicator.stopAnimating()
|
||
loadingIndicator.removeFromSuperview()
|
||
}
|
||
|
||
private func ensureEPUBPaginationHostView(viewportSize: CGSize) -> UIView {
|
||
let hostFrame = CGRect(
|
||
x: -viewportSize.width - 32,
|
||
y: 0,
|
||
width: max(viewportSize.width, 1),
|
||
height: max(viewportSize.height, 1)
|
||
)
|
||
|
||
if epubPaginationHostView.superview == nil {
|
||
view.addSubview(epubPaginationHostView)
|
||
}
|
||
epubPaginationHostView.frame = hostFrame
|
||
view.sendSubviewToBack(epubPaginationHostView)
|
||
return epubPaginationHostView
|
||
}
|
||
|
||
private func ensureEPUBPreloadHostView(viewportSize: CGSize) -> UIView {
|
||
let hostFrame = CGRect(
|
||
x: -viewportSize.width - 32,
|
||
y: 0,
|
||
width: max(viewportSize.width, 1),
|
||
height: max(viewportSize.height, 1)
|
||
)
|
||
|
||
if epubPreloadHostView.superview == nil {
|
||
view.addSubview(epubPreloadHostView)
|
||
}
|
||
epubPreloadHostView.frame = hostFrame
|
||
view.sendSubviewToBack(epubPreloadHostView)
|
||
return epubPreloadHostView
|
||
}
|
||
|
||
private func clearEPUBPreloadCache() {
|
||
epubPreloadViews.values.forEach {
|
||
$0.releaseResources()
|
||
$0.removeFromSuperview()
|
||
}
|
||
epubPreloadViews.removeAll()
|
||
epubPageCurlViews.values.forEach {
|
||
$0.releaseResources()
|
||
$0.removeFromSuperview()
|
||
}
|
||
epubPageCurlViews.removeAll()
|
||
}
|
||
|
||
private func releaseEPUBPageViews(except keepPages: Set<Int>) {
|
||
epubPreloadViews.forEach { key, value in
|
||
guard !keepPages.contains(key) else { return }
|
||
value.releaseResources()
|
||
value.removeFromSuperview()
|
||
}
|
||
epubPreloadViews = epubPreloadViews.filter { key, _ in
|
||
keepPages.contains(key)
|
||
}
|
||
|
||
epubPageCurlViews.forEach { key, value in
|
||
guard !keepPages.contains(key) else { return }
|
||
value.releaseResources()
|
||
value.removeFromSuperview()
|
||
}
|
||
epubPageCurlViews = epubPageCurlViews.filter { key, _ in
|
||
keepPages.contains(key)
|
||
}
|
||
}
|
||
|
||
private func trimEPUBPageCurlCache(around pageNum: Int) {
|
||
let keepPages = Set(
|
||
[
|
||
pageNum - 1,
|
||
pageNum,
|
||
pageNum + 1,
|
||
].filter { index in
|
||
index > 0
|
||
&& index < pageCountOfReaderView(
|
||
readerView: readerView
|
||
) - 1
|
||
}
|
||
)
|
||
releaseEPUBPageViews(except: keepPages)
|
||
}
|
||
|
||
private func primeEPUBPageCache(around pageNum: Int) {
|
||
guard case .epubFile = bookSource,
|
||
let publication = epubPublication,
|
||
publication.layout != .fixed,
|
||
pageNum > 0,
|
||
pageNum < pageCountOfReaderView(readerView: readerView) - 1
|
||
else {
|
||
return
|
||
}
|
||
|
||
let validPages = Set(
|
||
[pageNum - 1, pageNum + 1].filter { index in
|
||
index > 0
|
||
&& index < pageCountOfReaderView(
|
||
readerView: readerView
|
||
) - 1
|
||
}
|
||
)
|
||
|
||
epubPreloadViews = epubPreloadViews.filter { key, value in
|
||
let shouldKeep = validPages.contains(key)
|
||
if !shouldKeep {
|
||
value.releaseResources()
|
||
value.removeFromSuperview()
|
||
}
|
||
return shouldKeep
|
||
}
|
||
|
||
let theme = RDReaderManager.shared.themeType.currentType.value
|
||
let viewportSize = currentEPUBLayoutContext().viewportSize
|
||
let preloadHostView = ensureEPUBPreloadHostView(
|
||
viewportSize: viewportSize
|
||
)
|
||
for targetPage in validPages.sorted() {
|
||
let contentView = configuredEPUBContentView(
|
||
pageNum: targetPage,
|
||
contentView: cachedEPUBPreloadView(pageNum: targetPage),
|
||
theme: theme,
|
||
publication: publication,
|
||
isInteractive: false
|
||
)
|
||
if contentView.superview == nil {
|
||
preloadHostView.addSubview(contentView)
|
||
contentView.frame = preloadHostView.bounds
|
||
}
|
||
}
|
||
}
|
||
|
||
private func cachedEPUBPreloadView(pageNum: Int) -> RDReaderEPUBContentView
|
||
{
|
||
if let cachedView = epubPreloadViews[pageNum] {
|
||
return cachedView
|
||
}
|
||
let contentView = RDReaderEPUBContentView()
|
||
epubPreloadViews[pageNum] = contentView
|
||
return contentView
|
||
}
|
||
|
||
private func cachedEPUBPageCurlView(pageNum: Int) -> RDReaderEPUBContentView
|
||
{
|
||
if let cachedView = epubPageCurlViews[pageNum] {
|
||
return cachedView
|
||
}
|
||
if let preloadedView = epubPreloadViews.removeValue(forKey: pageNum) {
|
||
preloadedView.removeFromSuperview()
|
||
epubPageCurlViews[pageNum] = preloadedView
|
||
return preloadedView
|
||
}
|
||
let contentView = RDReaderEPUBContentView()
|
||
epubPageCurlViews[pageNum] = contentView
|
||
return contentView
|
||
}
|
||
|
||
private func configuredEPUBContentView(
|
||
pageNum: Int,
|
||
contentView: RDReaderEPUBContentView,
|
||
theme: RDReaderTheme,
|
||
publication: RDEPUBPublication,
|
||
isInteractive: Bool = true
|
||
) -> RDReaderEPUBContentView {
|
||
let epubPage = epubPages[pageNum - 1]
|
||
contentView.delegate = isInteractive ? self : nil
|
||
contentView.backgroundColor = theme.contentBackgroudColor
|
||
contentView.pageNumLabel.textColor = theme.contentTextColor
|
||
contentView.pageNumLabel.text = "\(pageNum) / \(epubPages.count)"
|
||
|
||
let isFixedLayout = publication.layout == .fixed
|
||
guard
|
||
let request = renderRequest(
|
||
for: pageNum,
|
||
page: epubPage,
|
||
publication: publication,
|
||
theme: theme
|
||
)
|
||
else {
|
||
return contentView
|
||
}
|
||
|
||
contentView.load(publication: publication, request: request)
|
||
return contentView
|
||
}
|
||
|
||
// MARK: - UI Setup
|
||
|
||
func makeUI() {
|
||
|
||
readerView.delegate = self
|
||
readerView.dataSource = self
|
||
readerView.landscapeDualPageEnabled = true
|
||
readerView.coverPageIndex = 0
|
||
readerView.pageDirection = .rightToLeft
|
||
readerView.frame = CGRect(
|
||
x: 0,
|
||
y: 0,
|
||
width: view.frame.width,
|
||
height: view.frame.height
|
||
)
|
||
readerView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||
view.addSubview(readerView)
|
||
|
||
readerView
|
||
.register(
|
||
contentView: RDReaderContentView.self,
|
||
contentViewWithReuseIdentifier: NSStringFromClass(
|
||
RDReaderContentView.self
|
||
)
|
||
)
|
||
readerView
|
||
.register(
|
||
contentView: RDReaderEPUBContentView.self,
|
||
contentViewWithReuseIdentifier: NSStringFromClass(
|
||
RDReaderEPUBContentView.self
|
||
)
|
||
)
|
||
readerView
|
||
.register(
|
||
contentView: RDReaderEPUBTextContentView.self,
|
||
contentViewWithReuseIdentifier: NSStringFromClass(
|
||
RDReaderEPUBTextContentView.self
|
||
)
|
||
)
|
||
readerView
|
||
.register(
|
||
contentView: RDReaderCoverView.self,
|
||
contentViewWithReuseIdentifier: NSStringFromClass(
|
||
RDReaderCoverView.self
|
||
)
|
||
)
|
||
readerView
|
||
.register(
|
||
contentView: RDReaderEndView.self,
|
||
contentViewWithReuseIdentifier: NSStringFromClass(
|
||
RDReaderEndView.self
|
||
)
|
||
)
|
||
|
||
topToolView.trigger { [weak self] event in
|
||
guard let self = self else { return }
|
||
switch event {
|
||
case .back:
|
||
self.navigationController?.popViewController(animated: true)
|
||
}
|
||
}
|
||
|
||
bottomToolView.trigger { [weak self] event in
|
||
guard let self = self else { return }
|
||
switch event {
|
||
case .chapterList:
|
||
self.showChapterListView()
|
||
case .highlight:
|
||
if case .epubFile = self.bookSource {
|
||
self.addHighlightFromCurrentSelection()
|
||
}
|
||
case .settings:
|
||
self.showEditView()
|
||
case .darkAndLight:
|
||
if RDReaderManager.shared.themeType.currentType != .dark {
|
||
RDReaderManager.shared.themeType.currentType = .dark
|
||
} else {
|
||
RDReaderManager.shared.themeType.currentType = .white
|
||
}
|
||
}
|
||
}
|
||
|
||
RDReaderManager.shared.themeType.observeChange { [weak self] theme in
|
||
guard let self = self else { return }
|
||
self.readerView.backgroundColor = theme.contentBackgroudColor
|
||
self.topToolView.backgroundColor = theme.toolBackgroudColor
|
||
self.bottomToolView.backgroundColor = theme.toolBackgroudColor
|
||
self.topToolView.lineView.backgroundColor = theme.toolLineColor
|
||
self.bottomToolView.lineView.backgroundColor = theme.toolLineColor
|
||
self.topToolView.backButton.tintColor = theme.toolControlTextColor
|
||
self.bottomToolView.chapterListButton.tintColor =
|
||
theme.toolControlTextColor
|
||
self.bottomToolView.highlightButton.tintColor =
|
||
theme.toolControlTextColor
|
||
self.bottomToolView.settingsButton.tintColor =
|
||
theme.toolControlTextColor
|
||
self.bottomToolView.lightModeButton.tintColor =
|
||
theme.toolControlTextColor
|
||
if self.isReaderContentReady {
|
||
self.readerView.reloadData()
|
||
}
|
||
}
|
||
|
||
RDReaderManager.shared.pageCurlTypeType
|
||
.observeChange { [weak self] type in
|
||
guard let self = self else { return }
|
||
self.readerView
|
||
.switchReaderDisplayType(
|
||
self.effectiveDisplayType(requested: type)
|
||
)
|
||
if case .epubFile = self.bookSource {
|
||
self.storeEPUBValidationState([
|
||
"validationReason": "displayTypeChanged",
|
||
"settingsDisplayType": RDReaderManager.shared
|
||
.pageCurlTypeType.currentType.rawValue,
|
||
"lastVisiblePage": self.readerView.currentPage,
|
||
])
|
||
}
|
||
}
|
||
|
||
RDReaderManager.shared.fontValue.observeChange { [weak self] _ in
|
||
guard let self = self else { return }
|
||
guard self.isReaderContentReady else { return }
|
||
switch self.bookSource {
|
||
case .textFile:
|
||
self.loadData()
|
||
self.readerView.reloadData()
|
||
case .epubFile:
|
||
self.repaginateEPUB(reason: "fontSizeChanged")
|
||
}
|
||
}
|
||
|
||
RDReaderManager.shared.lineSpaceType.observeChange { [weak self] _ in
|
||
guard let self = self else { return }
|
||
guard self.isReaderContentReady else { return }
|
||
switch self.bookSource {
|
||
case .textFile:
|
||
self.loadData()
|
||
self.readerView.reloadData()
|
||
case .epubFile:
|
||
self.repaginateEPUB(reason: "lineSpacingChanged")
|
||
}
|
||
}
|
||
}
|
||
|
||
func showChapterListView() {
|
||
switch bookSource {
|
||
case .textFile:
|
||
showTXTChapterList()
|
||
case .epubFile:
|
||
showEPUBChapterList()
|
||
}
|
||
}
|
||
|
||
private func showTXTChapterList() {
|
||
let chapterListVC = SSChapterListController(
|
||
chapters: self.book.chapters,
|
||
currentChapterNum: currentChapterNum
|
||
)
|
||
chapterListVC.view.ss_w = view.frame.width * 0.8
|
||
chapterListVC.view.ss_h = view.frame.height
|
||
|
||
let alertView = SSAlertView(
|
||
customView: chapterListVC.view,
|
||
fromViewController: self,
|
||
canPanDimiss: false
|
||
)
|
||
let animation = SSAlertDefaultAnmation(state: .fromLeft)
|
||
animation.isSpringShowAnimation = false
|
||
alertView.animation = animation
|
||
alertView.show()
|
||
|
||
chapterListVC.selectedChapter { [weak alertView, weak self] number in
|
||
guard let self = self else { return }
|
||
let chapter = self.book.chapters[number]
|
||
self.transitionToPage(rangeLocation: chapter.range.location)
|
||
alertView?.hide()
|
||
}
|
||
}
|
||
|
||
private func showEPUBChapterList() {
|
||
let currentIndex = currentEPUBTOCIndex()
|
||
let chapters: [Chapter]
|
||
if !epubTOCItems.isEmpty {
|
||
chapters =
|
||
epubTOCItems
|
||
.enumerated()
|
||
.map { (index, item) -> Chapter in
|
||
let indent = String(repeating: " ", count: item.depth)
|
||
return Chapter(
|
||
sort: index + 1,
|
||
title: indent + item.title,
|
||
pageCount: 0,
|
||
content: "",
|
||
pages: [],
|
||
range: NSMakeRange(0, 0)
|
||
)
|
||
}
|
||
} else if isTextEPUBMode, let textBook = epubTextBook {
|
||
chapters = textBook.chapters.enumerated().map { index, chapter in
|
||
Chapter(
|
||
sort: index + 1,
|
||
title: chapter.title,
|
||
pageCount: chapter.pages.count,
|
||
content: "",
|
||
pages: [],
|
||
range: NSMakeRange(0, 0)
|
||
)
|
||
}
|
||
} else {
|
||
chapters =
|
||
epubChapters
|
||
.enumerated()
|
||
.map { (index, info) -> Chapter in
|
||
Chapter(
|
||
sort: index + 1,
|
||
title: info.title,
|
||
pageCount: info.pageCount,
|
||
content: "",
|
||
pages: [],
|
||
range: NSMakeRange(0, 0)
|
||
)
|
||
}
|
||
}
|
||
let chapterListVC = SSChapterListController(
|
||
chapters: chapters,
|
||
currentChapterNum: currentIndex
|
||
)
|
||
chapterListVC.view.ss_w = view.frame.width * 0.8
|
||
chapterListVC.view.ss_h = view.frame.height
|
||
|
||
let alertView = SSAlertView(
|
||
customView: chapterListVC.view,
|
||
fromViewController: self,
|
||
canPanDimiss: false
|
||
)
|
||
let animation = SSAlertDefaultAnmation(state: .fromLeft)
|
||
animation.isSpringShowAnimation = false
|
||
alertView.animation = animation
|
||
alertView.show()
|
||
|
||
chapterListVC.selectedChapter { [weak alertView, weak self] number in
|
||
guard let self = self else { return }
|
||
if !self.epubTOCItems.isEmpty {
|
||
let item = self.epubTOCItems[number]
|
||
if self.isTextEPUBMode, let pageNumber = item.pageNumber {
|
||
self.storeEPUBTOCValidation(
|
||
item: item,
|
||
resolvedPage: pageNumber
|
||
)
|
||
self.readerView.transitionToPage(pageNum: pageNumber)
|
||
} else {
|
||
let location = RDEPUBLocation(
|
||
bookIdentifier: self.currentEPUBBookIdentifier,
|
||
href: item.href,
|
||
progression: 0,
|
||
fragment: nil
|
||
)
|
||
let fromSpineIndex =
|
||
self.readerView.currentPage > 0
|
||
&& self.epubPages.indices.contains(
|
||
self.readerView.currentPage - 1
|
||
)
|
||
? self.epubPages[self.readerView.currentPage - 1]
|
||
.spineIndex
|
||
: 0
|
||
let resolvedPage = self.epubSession?.pageIndex(
|
||
for: location,
|
||
bookIdentifier: self.currentEPUBBookIdentifier
|
||
).map {
|
||
$0 + 1
|
||
}
|
||
self.storeEPUBTOCValidation(
|
||
item: item,
|
||
resolvedPage: resolvedPage
|
||
)
|
||
self.navigateToEPUBLink(
|
||
location,
|
||
fromSpineIndex: fromSpineIndex
|
||
)
|
||
}
|
||
} else if self.isTextEPUBMode,
|
||
let textPage = self.epubTextBook?.pages.first(
|
||
where: {
|
||
$0.chapterIndex == number
|
||
})
|
||
{
|
||
self.readerView
|
||
.transitionToPage(pageNum: textPage.absolutePageIndex + 1)
|
||
} else if let pageIndex = self.epubPages.firstIndex(
|
||
where: {
|
||
$0.chapterIndex == number
|
||
})
|
||
{
|
||
self.readerView.transitionToPage(pageNum: pageIndex + 1)
|
||
}
|
||
alertView?.hide()
|
||
}
|
||
}
|
||
|
||
func showEditView() {
|
||
let customView = RDReaderEditView(
|
||
frame: CGRect(x: 0, y: 0, width: view.frame.width, height: 300)
|
||
)
|
||
customView.brightSlider
|
||
.setValue(
|
||
Float(RDReaderManager.shared.brightValue.currentType),
|
||
animated: false
|
||
)
|
||
customView.fontView.currentFontSize =
|
||
RDReaderManager.shared.fontValue.currentType
|
||
customView.lineSpaceView.currentType =
|
||
RDReaderManager.shared.lineSpaceType.currentType
|
||
customView.pageCurlTypeView.currentType =
|
||
RDReaderManager.shared.pageCurlTypeType.currentType
|
||
customView.colorView.currentType =
|
||
RDReaderManager.shared.themeType.currentType
|
||
|
||
let alertView = SSAlertView(
|
||
customView: customView,
|
||
fromViewController: self,
|
||
animation: SSAlertDefaultAnmation(state: .fromBottom),
|
||
canPanDimiss: false
|
||
)
|
||
alertView.show()
|
||
|
||
customView.brightSlider.trigger { event in
|
||
switch event {
|
||
case .valueDidChange(let value):
|
||
RDReaderManager.shared.brightValue.currentType = value
|
||
}
|
||
}
|
||
|
||
customView.fontView.trigger { event in
|
||
switch event {
|
||
case .fontSize(let size):
|
||
RDReaderManager.shared.fontValue.currentType = size
|
||
}
|
||
}
|
||
|
||
customView.lineSpaceView.trigger { event in
|
||
switch event {
|
||
case .chose(let type):
|
||
RDReaderManager.shared.lineSpaceType.currentType = type
|
||
}
|
||
}
|
||
|
||
customView.pageCurlTypeView.trigger { event in
|
||
switch event {
|
||
case .chose(let type):
|
||
RDReaderManager.shared.pageCurlTypeType.currentType = type
|
||
}
|
||
}
|
||
|
||
customView.colorView.trigger { event in
|
||
switch event {
|
||
case .chose(let type):
|
||
RDReaderManager.shared.themeType.currentType = type
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: - RDReaderDataSource & RDReaderDelegate
|
||
|
||
extension RDReaderController: RDReaderDataSource, RDReaderDelegate {
|
||
|
||
public func pageIdentifier(readerView: RDReaderView, pageNum: Int)
|
||
-> String?
|
||
{
|
||
if pageNum == 0 {
|
||
return NSStringFromClass(RDReaderCoverView.self)
|
||
} else if pageNum == pageCountOfReaderView(readerView: readerView) - 1 {
|
||
return NSStringFromClass(RDReaderEndView.self)
|
||
}
|
||
switch bookSource {
|
||
case .textFile:
|
||
return NSStringFromClass(RDReaderContentView.self)
|
||
case .epubFile:
|
||
return isTextEPUBMode
|
||
? NSStringFromClass(RDReaderEPUBTextContentView.self)
|
||
: NSStringFromClass(RDReaderEPUBContentView.self)
|
||
}
|
||
}
|
||
|
||
public func pageCountOfReaderView(readerView: RDReaderView) -> Int {
|
||
switch bookSource {
|
||
case .textFile:
|
||
return chapterPages.count + 2 // +cover +end
|
||
case .epubFile:
|
||
return
|
||
(isTextEPUBMode
|
||
? (epubTextBook?.pages.count ?? 0) : epubPages.count) + 2
|
||
}
|
||
}
|
||
|
||
public func pageContentView(
|
||
readerView: RDReaderView,
|
||
pageNum: Int,
|
||
containerView: UIView?
|
||
) -> UIView {
|
||
let totalPages = pageCountOfReaderView(readerView: readerView)
|
||
let theme = RDReaderManager.shared.themeType.currentType.value
|
||
|
||
// 封面页
|
||
if pageNum == 0 {
|
||
let contentView =
|
||
(containerView as? RDReaderCoverView) ?? RDReaderCoverView()
|
||
contentView.backgroundColor = theme.contentBackgroudColor
|
||
contentView.textLabel.textColor = theme.contentTextColor
|
||
switch bookSource {
|
||
case .textFile:
|
||
contentView.textLabel.text = book.desc
|
||
case .epubFile:
|
||
contentView.textLabel.text =
|
||
epubParser?.metadata.title ?? "EPUB"
|
||
if let coverImage = epubParser?.coverImage() {
|
||
contentView.imageV.image = coverImage
|
||
}
|
||
}
|
||
return contentView
|
||
}
|
||
|
||
// 结束页
|
||
if pageNum == totalPages - 1 {
|
||
let contentView =
|
||
(containerView as? RDReaderEndView) ?? RDReaderEndView()
|
||
contentView.backgroundColor = theme.contentBackgroudColor
|
||
return contentView
|
||
}
|
||
|
||
// 内容页
|
||
switch bookSource {
|
||
case .textFile:
|
||
return txtContentView(
|
||
pageNum: pageNum,
|
||
containerView: containerView,
|
||
theme: theme
|
||
)
|
||
case .epubFile:
|
||
return isTextEPUBMode
|
||
? epubTextContentView(
|
||
pageNum: pageNum,
|
||
containerView: containerView,
|
||
theme: theme
|
||
)
|
||
: epubContentView(
|
||
pageNum: pageNum,
|
||
containerView: containerView,
|
||
theme: theme
|
||
)
|
||
}
|
||
}
|
||
|
||
/// TXT内容页
|
||
private func txtContentView(
|
||
pageNum: Int,
|
||
containerView: UIView?,
|
||
theme: RDReaderTheme
|
||
) -> UIView {
|
||
let chapter = chapterPages[pageNum - 1]
|
||
let contentView =
|
||
(containerView as? RDReaderContentView) ?? RDReaderContentView()
|
||
contentView.textLabel.textColor = theme.contentTextColor
|
||
contentView.pageNumLabel.textColor = theme.contentTextColor
|
||
contentView.backgroundColor = theme.contentBackgroudColor
|
||
contentView.textLabel.attributedText = chapter.attrContent
|
||
contentView.pageNumLabel.text = "\(pageNum) / \(chapterPages.count)"
|
||
return contentView
|
||
}
|
||
|
||
/// EPUB内容页(WKWebView渲染)
|
||
private func epubContentView(
|
||
pageNum: Int,
|
||
containerView: UIView?,
|
||
theme: RDReaderTheme
|
||
) -> UIView {
|
||
guard let publication = epubPublication else {
|
||
return containerView ?? UIView()
|
||
}
|
||
|
||
let contentView: RDReaderEPUBContentView
|
||
if readerView.currentDisplayType == .pageCurl, containerView == nil {
|
||
contentView = cachedEPUBPageCurlView(pageNum: pageNum)
|
||
} else {
|
||
contentView =
|
||
(containerView as? RDReaderEPUBContentView)
|
||
?? RDReaderEPUBContentView()
|
||
}
|
||
|
||
return configuredEPUBContentView(
|
||
pageNum: pageNum,
|
||
contentView: contentView,
|
||
theme: theme,
|
||
publication: publication,
|
||
isInteractive: true
|
||
)
|
||
}
|
||
|
||
private func epubTextContentView(
|
||
pageNum: Int,
|
||
containerView: UIView?,
|
||
theme: RDReaderTheme
|
||
) -> UIView {
|
||
guard let page = epubTextBook?.page(at: pageNum) else {
|
||
return containerView ?? UIView()
|
||
}
|
||
let contentView =
|
||
(containerView as? RDReaderEPUBTextContentView)
|
||
?? RDReaderEPUBTextContentView()
|
||
contentView
|
||
.configure(
|
||
page: page,
|
||
pageNumber: pageNum,
|
||
totalPages: epubTextBook?.pages.count ?? 0,
|
||
theme: theme
|
||
)
|
||
return contentView
|
||
}
|
||
|
||
public func pageNum(readerView: RDReaderView, pageNum: Int) {
|
||
switch bookSource {
|
||
case .textFile:
|
||
if pageNum > 0 && chapterPages.count > pageNum - 1 {
|
||
let chapterPage = chapterPages[pageNum - 1]
|
||
let chapter = book.chapters[chapterPage.chapterSort - 1]
|
||
let chapterRange = chapter.range
|
||
let rangeLoc =
|
||
chapterRange.location + chapterPage.range.location
|
||
RDReaderManager.shared.readProgress.currentType = rangeLoc
|
||
currentChapterNum = chapterPage.chapterSort - 1
|
||
}
|
||
case .epubFile:
|
||
let readablePageCount = currentReadableEPUBPageCount()
|
||
if readablePageCount > 0 {
|
||
if pageNum == readablePageCount {
|
||
storeEPUBEndValidationState(
|
||
pageNum: pageNum,
|
||
reachedEndScreen: false
|
||
)
|
||
} else if pageNum == readablePageCount + 1 {
|
||
storeEPUBEndValidationState(
|
||
pageNum: pageNum,
|
||
reachedEndScreen: true
|
||
)
|
||
}
|
||
}
|
||
if isTextEPUBMode {
|
||
guard pageNum > 0,
|
||
let page = epubTextBook?.page(at: pageNum),
|
||
let location = epubTextBook?.location(
|
||
forPageNumber: pageNum,
|
||
bookIdentifier: currentEPUBBookIdentifier
|
||
)
|
||
else {
|
||
return
|
||
}
|
||
currentChapterNum = page.chapterIndex
|
||
saveEPUBLocation(location)
|
||
saveEPUBSelection(nil)
|
||
epubSession?.transition(to: .idle)
|
||
return
|
||
}
|
||
|
||
if pageNum > 0 && epubPages.count > pageNum - 1 {
|
||
let epubPage = epubPages[pageNum - 1]
|
||
if epubSession?.navigatorState == .idle {
|
||
epubSession?.transition(to: .moving)
|
||
}
|
||
currentChapterNum = epubPage.chapterIndex
|
||
if let location = fallbackEPUBLocation(for: epubPage) {
|
||
saveEPUBLocation(location)
|
||
}
|
||
saveEPUBSelection(nil)
|
||
schedulePendingBackgroundEPUBPaginationApplication()
|
||
primeEPUBPageCache(around: pageNum)
|
||
trimEPUBPageCurlCache(around: pageNum)
|
||
if epubSession?.pendingNavigationPageNum == nil {
|
||
epubSession?.transition(to: .idle)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
public func topToolView(readerView: RDReaderView) -> UIView? {
|
||
return topToolView
|
||
}
|
||
|
||
public func bottomToolView(readerView: RDReaderView) -> UIView? {
|
||
return bottomToolView
|
||
}
|
||
|
||
/// 横竖屏切换时重新分页
|
||
public func readerViewOrientationWillChange(
|
||
readerView: RDReaderView,
|
||
isLandscape: Bool
|
||
) {
|
||
switch bookSource {
|
||
case .textFile:
|
||
loadData()
|
||
transitionToPage()
|
||
case .epubFile:
|
||
repaginateEPUB(reason: "orientationChanged")
|
||
}
|
||
}
|
||
}
|
||
|
||
extension RDReaderController: RDReaderEPUBContentViewDelegate {
|
||
func epubContentView(
|
||
_ contentView: RDReaderEPUBContentView,
|
||
didUpdateLocation location: RDEPUBLocation,
|
||
spineIndex: Int
|
||
) {
|
||
guard readerView.currentPage > 0,
|
||
epubPages.indices
|
||
.contains(readerView.currentPage - 1)
|
||
else {
|
||
return
|
||
}
|
||
let currentPage = epubPages[readerView.currentPage - 1]
|
||
guard
|
||
epubSession?
|
||
.pageContains(spineIndex: spineIndex, in: currentPage) == true
|
||
else {
|
||
return
|
||
}
|
||
saveEPUBLocation(location)
|
||
epubSession?.updateReadingContext(
|
||
pageNumber: readerView.currentPage,
|
||
location: location,
|
||
spineIndex: spineIndex,
|
||
chapterIndex: currentPage.chapterIndex,
|
||
bookIdentifier: currentEPUBBookIdentifier
|
||
)
|
||
}
|
||
|
||
func epubContentView(
|
||
_ contentView: RDReaderEPUBContentView,
|
||
didChangeSelection selection: RDEPUBSelection?,
|
||
spineIndex: Int
|
||
) {
|
||
guard readerView.currentPage > 0,
|
||
epubPages.indices
|
||
.contains(readerView.currentPage - 1)
|
||
else {
|
||
return
|
||
}
|
||
let currentPage = epubPages[readerView.currentPage - 1]
|
||
guard
|
||
epubSession?
|
||
.pageContains(spineIndex: spineIndex, in: currentPage) == true
|
||
else {
|
||
return
|
||
}
|
||
saveEPUBSelection(selection)
|
||
}
|
||
|
||
func epubContentView(
|
||
_ contentView: RDReaderEPUBContentView,
|
||
didActivateInternalLink location: RDEPUBLocation,
|
||
fromSpineIndex: Int
|
||
) {
|
||
saveEPUBSelection(nil)
|
||
navigateToEPUBLink(location, fromSpineIndex: fromSpineIndex)
|
||
}
|
||
|
||
func epubContentView(
|
||
_ contentView: RDReaderEPUBContentView,
|
||
didActivateExternalLink url: URL
|
||
) {
|
||
UIApplication.shared.open(url, options: [:], completionHandler: nil)
|
||
}
|
||
|
||
func epubContentView(
|
||
_ contentView: RDReaderEPUBContentView,
|
||
didLogJavaScriptError message: String
|
||
) {
|
||
print("EPUB JS Error: \(message)")
|
||
}
|
||
}
|