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

2142 lines
73 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// RDReaderController.swift
// RDReaderDemo
//
// Created by yangsq on 2021/8/10.
//
import SSAlertSwift
import UIKit
///
public enum BookSource {
/// TXTBundle
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
}
/// EPUBWKWebView
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)")
}
}