Files
ReadViewSDK/Sources/RDReaderView/LegacyRDReaderController/RDReaderController.swift
T
2026-05-21 19:40:51 +08:00

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)")
}
}