refactor: rename RDReaderView -> RDEpubReaderView, update pod config and docs
- Rename source module from RDReaderView to RDEpubReaderView - Move all source files from Sources/RDReaderView/ to Sources/RDEpubReaderView/ - Update podspec: RDReaderView.podspec -> RDEpubReaderView.podspec - Update Podfile, demo project, and CocoaPods config for new pod name - Delete old RDReaderView pod support files from ReadViewDemo/Pods - Add new RDEpubReaderView pod support files - Update documentation (API ref, architecture, UML, conventions, etc.) - Add FixedLayoutRotationTests - Update .gitignore: exclude .DS_Store, manual unpack backups, _ssoft-output
This commit is contained in:
@@ -0,0 +1,516 @@
|
||||
|
||||
import UIKit
|
||||
|
||||
public final class RDEpubURLReaderController: UIViewController {
|
||||
private struct PendingDemoPageRequest {
|
||||
let pageNumber: Int
|
||||
let animated: Bool
|
||||
var attemptCount: Int
|
||||
}
|
||||
|
||||
private let bookURL: URL
|
||||
|
||||
private let epubConfiguration: RDEPUBReaderConfiguration
|
||||
|
||||
private var embeddedController: UIViewController?
|
||||
private let demoStateLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.accessibilityIdentifier = "demo.reader.state"
|
||||
label.font = .systemFont(ofSize: 1)
|
||||
label.textColor = .clear
|
||||
label.alpha = 0.01
|
||||
label.isAccessibilityElement = true
|
||||
label.text = "reader=opening"
|
||||
return label
|
||||
}()
|
||||
private var pendingDemoPageRequest: PendingDemoPageRequest?
|
||||
private var isRetryingPendingDemoPage = false
|
||||
private let maxPendingDemoPageAttempts = 24
|
||||
private let pendingDemoPageRetryDelay: TimeInterval = 0.25
|
||||
private var demoStateTimer: Timer?
|
||||
private var lastEmittedDemoState = ""
|
||||
private var pendingSearchKeyword: String?
|
||||
private var externalLinkActivationCount = 0
|
||||
private var lastActivatedExternalURL: URL?
|
||||
private var lastReaderErrorDescription = "none"
|
||||
|
||||
public init(
|
||||
bookURL: URL,
|
||||
epubConfiguration: RDEPUBReaderConfiguration = RDEPUBReaderConfiguration()
|
||||
) {
|
||||
self.bookURL = bookURL
|
||||
self.epubConfiguration = epubConfiguration
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
public override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
view.backgroundColor = .systemBackground
|
||||
title = bookURL.deletingPathExtension().lastPathComponent
|
||||
RDEPUBResourceURLSchemeHandler.resetDebugMetrics()
|
||||
embedReaderController()
|
||||
installDemoStateLabel()
|
||||
}
|
||||
|
||||
public override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
startDemoStateTimerIfNeeded()
|
||||
refreshDemoState()
|
||||
}
|
||||
|
||||
public override func viewDidDisappear(_ animated: Bool) {
|
||||
super.viewDidDisappear(animated)
|
||||
stopDemoStateTimer()
|
||||
}
|
||||
|
||||
deinit {
|
||||
stopDemoStateTimer()
|
||||
}
|
||||
|
||||
public func applyDemoDisplayType(_ displayType: RDEpubReaderView.DisplayType) {
|
||||
readerController?.configuration.displayType = displayType
|
||||
emitDemoState(prefix: "display=\(displayType.demoArgumentValue)")
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func goToDemoPage(_ pageNumber: Int, animated: Bool = false) -> Bool {
|
||||
let moved = performDemoPageNavigation(pageNumber, animated: animated)
|
||||
if moved {
|
||||
pendingDemoPageRequest = nil
|
||||
isRetryingPendingDemoPage = false
|
||||
} else {
|
||||
queuePendingDemoPageNavigation(pageNumber, animated: animated)
|
||||
}
|
||||
return moved
|
||||
}
|
||||
|
||||
public func runDemoDisplaySequence(
|
||||
_ displayTypes: [RDEpubReaderView.DisplayType],
|
||||
initialPageNumber: Int? = nil,
|
||||
stepDelay: TimeInterval = 1.0
|
||||
) {
|
||||
let normalizedDelay = max(stepDelay, 0.1)
|
||||
if let initialPageNumber {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + normalizedDelay) { [weak self] in
|
||||
_ = self?.goToDemoPage(initialPageNumber)
|
||||
}
|
||||
}
|
||||
|
||||
for (index, displayType) in displayTypes.enumerated() {
|
||||
let delay = normalizedDelay * Double(index + 1 + (initialPageNumber == nil ? 0 : 1))
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
|
||||
self?.applyDemoDisplayType(displayType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func performDemoSearch(keyword: String) {
|
||||
guard let readerController else { return }
|
||||
readerController.showSearchBar()
|
||||
if readerController.parser != nil || readerController.textBook != nil {
|
||||
submitSearchAfterDelay(keyword: keyword, retries: 10)
|
||||
} else {
|
||||
pendingSearchKeyword = keyword
|
||||
}
|
||||
}
|
||||
|
||||
private func submitSearchAfterDelay(keyword: String, retries: Int = 0) {
|
||||
guard let readerController else { return }
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
|
||||
guard let self else { return }
|
||||
readerController.search(keyword: keyword)
|
||||
readerController.updateSearchCount()
|
||||
// 按需加载的书打开初期搜索引擎可能尚未就绪(bookPageMap 未建立),
|
||||
// 只要还没有命中就继续重试,交由搜索自身的 token 去重。
|
||||
if retries > 0,
|
||||
readerController.searchState?.matches.isEmpty != false {
|
||||
self.submitSearchAfterDelay(keyword: keyword, retries: retries - 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func executePendingSearchIfNeeded() {
|
||||
guard let keyword = pendingSearchKeyword else { return }
|
||||
guard let readerController, readerController.parser != nil || readerController.textBook != nil else {
|
||||
return
|
||||
}
|
||||
pendingSearchKeyword = nil
|
||||
submitSearchAfterDelay(keyword: keyword, retries: 10)
|
||||
}
|
||||
|
||||
private func embedReaderController() {
|
||||
let controller: UIViewController
|
||||
if bookURL.pathExtension.lowercased() == "epub" {
|
||||
controller = RDEPUBReaderController(
|
||||
epubURL: bookURL,
|
||||
configuration: epubConfiguration
|
||||
)
|
||||
} else {
|
||||
let bookIdentifier = bookURL.lastPathComponent
|
||||
let bookTitle = bookURL.deletingPathExtension().lastPathComponent
|
||||
let pageSize = currentTextPageSize()
|
||||
let renderStyle = currentTextRenderStyle()
|
||||
let safeInsets = RDEPUBSafeArea.resolve(view.safeAreaInsets)
|
||||
let edgeInsets = UIEdgeInsets(
|
||||
top: max(epubConfiguration.reflowableContentInsets.top, safeInsets.top),
|
||||
left: max(epubConfiguration.reflowableContentInsets.left, safeInsets.left),
|
||||
bottom: max(epubConfiguration.reflowableContentInsets.bottom, safeInsets.bottom),
|
||||
right: max(epubConfiguration.reflowableContentInsets.right, safeInsets.right)
|
||||
)
|
||||
let builder = RDEpubPlainTextBookBuilder(
|
||||
layoutConfig: RDEPUBTextLayoutConfig(
|
||||
frameWidth: pageSize.width,
|
||||
frameHeight: pageSize.height,
|
||||
edgeInsets: edgeInsets,
|
||||
numberOfColumns: 1,
|
||||
columnGap: 20,
|
||||
avoidOrphans: false,
|
||||
avoidWidows: false,
|
||||
avoidPageBreakInsideEnabled: true,
|
||||
hyphenation: true,
|
||||
imageMaxHeightRatio: 0.85
|
||||
)
|
||||
)
|
||||
if let textBook = try? builder.build(textFileURL: bookURL, pageSize: pageSize, style: renderStyle) {
|
||||
controller = RDEPUBReaderController(
|
||||
textBook: textBook,
|
||||
bookIdentifier: bookIdentifier,
|
||||
title: bookTitle,
|
||||
textFileURL: bookURL,
|
||||
configuration: epubConfiguration
|
||||
)
|
||||
} else {
|
||||
|
||||
let fallback = UIViewController()
|
||||
let textView = UITextView()
|
||||
textView.isEditable = false
|
||||
textView.text = rd_decodeTextFile(url: bookURL)
|
||||
fallback.view = textView
|
||||
controller = fallback
|
||||
}
|
||||
}
|
||||
embeddedController = controller
|
||||
addChild(controller)
|
||||
controller.view.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(controller.view)
|
||||
NSLayoutConstraint.activate([
|
||||
controller.view.topAnchor.constraint(equalTo: view.topAnchor),
|
||||
controller.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
controller.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
controller.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||
])
|
||||
controller.didMove(toParent: self)
|
||||
readerController?.delegate = self
|
||||
emitDemoState()
|
||||
}
|
||||
|
||||
private var readerController: RDEPUBReaderController? {
|
||||
embeddedController as? RDEPUBReaderController
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
private func performDemoPageNavigation(_ pageNumber: Int, animated: Bool) -> Bool {
|
||||
guard let readerController else { return false }
|
||||
guard pageNumber > 0 else { return false }
|
||||
|
||||
let knownPages = readerController.readerContext.bookPageMap?.totalPages
|
||||
?? readerController.textBook?.pages.count
|
||||
?? readerController.activePages.count
|
||||
let numPages = readerController.readerView.numberOfPages()
|
||||
let moved = readerController.go(toPageNumber: pageNumber, animated: animated)
|
||||
if moved {
|
||||
emitDemoState(prefix: "page=\(pageNumber)")
|
||||
}
|
||||
return moved
|
||||
}
|
||||
|
||||
private func queuePendingDemoPageNavigation(_ pageNumber: Int, animated: Bool) {
|
||||
if let pendingDemoPageRequest,
|
||||
pendingDemoPageRequest.pageNumber == pageNumber,
|
||||
pendingDemoPageRequest.animated == animated {
|
||||
return
|
||||
}
|
||||
|
||||
pendingDemoPageRequest = PendingDemoPageRequest(
|
||||
pageNumber: pageNumber,
|
||||
animated: animated,
|
||||
attemptCount: 0
|
||||
)
|
||||
retryPendingDemoPageIfNeeded()
|
||||
}
|
||||
|
||||
private func retryPendingDemoPageIfNeeded() {
|
||||
guard !isRetryingPendingDemoPage else { return }
|
||||
guard pendingDemoPageRequest != nil else { return }
|
||||
|
||||
isRetryingPendingDemoPage = true
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + pendingDemoPageRetryDelay) { [weak self] in
|
||||
self?.attemptPendingDemoPageNavigation()
|
||||
}
|
||||
}
|
||||
|
||||
private func attemptPendingDemoPageNavigation() {
|
||||
guard var pendingDemoPageRequest else {
|
||||
isRetryingPendingDemoPage = false
|
||||
return
|
||||
}
|
||||
|
||||
if performDemoPageNavigation(pendingDemoPageRequest.pageNumber, animated: pendingDemoPageRequest.animated) {
|
||||
self.pendingDemoPageRequest = nil
|
||||
isRetryingPendingDemoPage = false
|
||||
return
|
||||
}
|
||||
|
||||
pendingDemoPageRequest.attemptCount += 1
|
||||
if pendingDemoPageRequest.attemptCount >= maxPendingDemoPageAttempts {
|
||||
self.pendingDemoPageRequest = nil
|
||||
isRetryingPendingDemoPage = false
|
||||
return
|
||||
}
|
||||
|
||||
self.pendingDemoPageRequest = pendingDemoPageRequest
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + pendingDemoPageRetryDelay) { [weak self] in
|
||||
self?.attemptPendingDemoPageNavigation()
|
||||
}
|
||||
}
|
||||
|
||||
private func logDemoState(prefix: String) {
|
||||
guard let readerController else { return }
|
||||
let location = readerController.currentLocation
|
||||
let href = location?.href ?? "nil"
|
||||
let progression = location.map { String(format: "%.4f", $0.navigationProgression) } ?? "nil"
|
||||
let page = readerController.currentPageNumber.map(String.init) ?? "nil"
|
||||
}
|
||||
|
||||
private func installDemoStateLabel() {
|
||||
view.addSubview(demoStateLabel)
|
||||
demoStateLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
demoStateLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
demoStateLabel.topAnchor.constraint(equalTo: view.topAnchor),
|
||||
demoStateLabel.widthAnchor.constraint(equalToConstant: 1),
|
||||
demoStateLabel.heightAnchor.constraint(equalToConstant: 1)
|
||||
])
|
||||
}
|
||||
|
||||
private func startDemoStateTimerIfNeeded() {
|
||||
guard demoStateTimer == nil else { return }
|
||||
demoStateTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { [weak self] _ in
|
||||
self?.refreshDemoState(logIfChanged: false)
|
||||
}
|
||||
if let demoStateTimer {
|
||||
RunLoop.main.add(demoStateTimer, forMode: .common)
|
||||
}
|
||||
}
|
||||
|
||||
private func stopDemoStateTimer() {
|
||||
demoStateTimer?.invalidate()
|
||||
demoStateTimer = nil
|
||||
}
|
||||
|
||||
private func emitDemoState(prefix: String? = nil) {
|
||||
refreshDemoState(logPrefix: prefix, logIfChanged: true)
|
||||
}
|
||||
|
||||
private func refreshDemoState(logPrefix: String? = nil, logIfChanged: Bool = true) {
|
||||
let page = readerController?.currentPageNumber.map(String.init) ?? "nil"
|
||||
let display = readerController?.configuration.displayType.demoArgumentValue ?? epubConfiguration.displayType.demoArgumentValue
|
||||
let toolbar = readerController?.readerView.isShowToolView == true ? "visible" : "hidden"
|
||||
let highlights = readerController?.highlights.count ?? 0
|
||||
let bookmarks = readerController?.activeBookmarks.count ?? 0
|
||||
let selection = readerController?.currentSelection == nil ? 0 : 1
|
||||
let location = readerController?.currentLocation
|
||||
let href = encodedDemoLocationHref(location?.href)
|
||||
let progression = location.map { String(format: "%.4f", $0.navigationProgression) } ?? "nil"
|
||||
let cfi = encodedDemoField(location?.cfi)
|
||||
let lastCFI = encodedDemoField(location?.lastCFI)
|
||||
let rangeCFI = encodedDemoField(location?.rangeCFI)
|
||||
let mapSnapshot = demoPaginationSnapshot()
|
||||
let layoutConfig = readerController?.readerContext.currentTextLayoutConfig(pageSize: currentTextPageSize())
|
||||
let resourceMetrics = RDEPUBResourceURLSchemeHandler.debugMetricsSnapshot()
|
||||
let cacheStats = readerController?.readerContext.makeChapterSummaryDiskCache().cacheStatistics
|
||||
?? (fileCount: 0, totalBytes: 0)
|
||||
let inspectable = readerController?.configuration.allowsInspectableWebViews ?? epubConfiguration.allowsInspectableWebViews
|
||||
let state = [
|
||||
"reader=opened",
|
||||
"page=\(page)",
|
||||
"display=\(display)",
|
||||
"toolbar=\(toolbar)",
|
||||
"highlights=\(highlights)",
|
||||
"bookmarks=\(bookmarks)",
|
||||
"selection=\(selection)",
|
||||
"href=\(href)",
|
||||
"progression=\(progression)",
|
||||
"cfi=\(cfi)",
|
||||
"lastCFI=\(lastCFI)",
|
||||
"rangeCFI=\(rangeCFI)",
|
||||
"mode=\(mapSnapshot.mode)",
|
||||
"pagination=\(mapSnapshot.phase)",
|
||||
"knownPages=\(mapSnapshot.knownPages)",
|
||||
"knownChapters=\(mapSnapshot.knownChapters)",
|
||||
"buildableChapters=\(mapSnapshot.buildableChapters)",
|
||||
"avoidWidows=\(layoutConfig?.avoidWidows == true ? 1 : 0)",
|
||||
"avoidOrphans=\(layoutConfig?.avoidOrphans == true ? 1 : 0)",
|
||||
"windowSize=\(readerController?.configuration.onDemandChapterWindowSize ?? epubConfiguration.onDemandChapterWindowSize)",
|
||||
"parseMs=\(readerController?.readerContext.lastMetadataParseWallClockMs ?? 0)",
|
||||
"parseConcurrency=\(readerController?.readerContext.lastMetadataParseConcurrency ?? 0)",
|
||||
"inspectable=\(inspectable ? 1 : 0)",
|
||||
"streamedResources=\(resourceMetrics.streamedResponses)",
|
||||
"inMemoryResources=\(resourceMetrics.inMemoryResponses)",
|
||||
"resourceFailures=\(resourceMetrics.failures)",
|
||||
"cacheFiles=\(cacheStats.fileCount)",
|
||||
"cacheBytes=\(cacheStats.totalBytes)",
|
||||
"externalLinks=\(externalLinkActivationCount)",
|
||||
"lastExternalURL=\(encodedDemoLocationHref(lastActivatedExternalURL?.absoluteString))",
|
||||
"lastError=\(encodedDemoField(lastReaderErrorDescription))",
|
||||
"searchMatchText=\(encodedDemoField(currentSearchMatchText()))",
|
||||
"footprintMB=\(String(format: "%.1f", RDEPUBMemoryProbe.footprintMB))"
|
||||
].joined(separator: " ")
|
||||
demoStateLabel.text = state
|
||||
if let logPrefix {
|
||||
logDemoState(prefix: logPrefix)
|
||||
} else if logIfChanged, state != lastEmittedDemoState {
|
||||
}
|
||||
lastEmittedDemoState = state
|
||||
}
|
||||
|
||||
private func encodedDemoLocationHref(_ href: String?) -> String {
|
||||
guard let href, !href.isEmpty else { return "nil" }
|
||||
return href.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? href.replacingOccurrences(of: " ", with: "%20")
|
||||
}
|
||||
|
||||
private func encodedDemoField(_ value: String?) -> String {
|
||||
guard let value, !value.isEmpty else { return "nil" }
|
||||
return value.addingPercentEncoding(withAllowedCharacters: .alphanumerics) ?? value.replacingOccurrences(of: " ", with: "_")
|
||||
}
|
||||
|
||||
private func currentSearchMatchText() -> String {
|
||||
guard let match = readerController?.searchState?.currentMatch else {
|
||||
return "none"
|
||||
}
|
||||
// Primary path: extract the exact matched text from the chapter content
|
||||
if let rangeLocation = match.rangeLocation,
|
||||
let chapterData = readerController?.textChapterData(forNormalizedHref: match.href) {
|
||||
let nsRange = NSRange(location: rangeLocation, length: match.rangeLength)
|
||||
if nsRange.location >= 0,
|
||||
nsRange.location + nsRange.length <= chapterData.attributedContent.length {
|
||||
return chapterData.attributedContent.attributedSubstring(from: nsRange).string
|
||||
}
|
||||
}
|
||||
// Fallback: chapter data may not be cached yet (on-demand loading).
|
||||
// If the match length equals the keyword length, the keyword itself
|
||||
// is the match text. Otherwise extract from previewText at the known offset.
|
||||
if let keyword = readerController?.searchState?.keyword,
|
||||
match.rangeLength == keyword.count {
|
||||
return keyword
|
||||
}
|
||||
// Final fallback: return "none" to indicate data not yet available
|
||||
return "none"
|
||||
}
|
||||
|
||||
private func demoPaginationSnapshot() -> (mode: String, phase: String, knownPages: Int, knownChapters: Int, buildableChapters: Int) {
|
||||
guard let readerController else {
|
||||
return ("unavailable", "none", 0, 0, 0)
|
||||
}
|
||||
|
||||
if let bookPageMap = readerController.readerContext.bookPageMap {
|
||||
let buildableChapters = readerController.publication?.spine.filter {
|
||||
$0.linear && ($0.mediaType.contains("html") || $0.mediaType.contains("xhtml"))
|
||||
}.count ?? 0
|
||||
let phase = buildableChapters > 0 && bookPageMap.totalChapters >= buildableChapters ? "full" : "partial"
|
||||
return ("bookPageMap", phase, bookPageMap.totalPages, bookPageMap.totalChapters, buildableChapters)
|
||||
}
|
||||
|
||||
if let textBook = readerController.textBook {
|
||||
return ("textBook", "full", textBook.pages.count, textBook.chapters.count, textBook.chapters.count)
|
||||
}
|
||||
|
||||
let snapshotChapters = readerController.activeChapters.count
|
||||
let snapshotPages = readerController.activePages.count
|
||||
return ("snapshot", snapshotPages > 0 ? "full" : "none", snapshotPages, snapshotChapters, snapshotChapters)
|
||||
}
|
||||
|
||||
private func currentTextPageSize() -> CGSize {
|
||||
UIScreen.main.bounds.size
|
||||
}
|
||||
|
||||
private func currentTextRenderStyle() -> RDEPUBTextRenderStyle {
|
||||
let font = epubConfiguration.fontChoice.font(ofSize: epubConfiguration.fontSize)
|
||||
let lineSpacing = max(font.lineHeight * (epubConfiguration.lineHeightMultiple - 1), 4)
|
||||
return RDEPUBTextRenderStyle(
|
||||
font: font,
|
||||
lineSpacing: lineSpacing,
|
||||
textColor: epubConfiguration.theme.contentTextColor,
|
||||
backgroundColor: epubConfiguration.theme.contentBackgroundColor
|
||||
)
|
||||
}
|
||||
|
||||
private func rd_decodeTextFile(url: URL) -> String {
|
||||
if let content = try? NSString(contentsOf: url, encoding: String.Encoding.utf8.rawValue) as String {
|
||||
return content
|
||||
}
|
||||
if let content = try? NSString(contentsOf: url, encoding: 0x80000632) as String {
|
||||
return content
|
||||
}
|
||||
if let content = try? NSString(contentsOf: url, encoding: 0x80000631) as String {
|
||||
return content
|
||||
}
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
extension RDEpubURLReaderController: RDEPUBReaderDelegate {
|
||||
public func epubReader(_ reader: UIViewController, didOpen publication: RDEPUBPublication) {
|
||||
retryPendingDemoPageIfNeeded()
|
||||
executePendingSearchIfNeeded()
|
||||
emitDemoState()
|
||||
}
|
||||
|
||||
public func epubReader(_ reader: UIViewController, didUpdateLocation location: RDEPUBLocation) {
|
||||
retryPendingDemoPageIfNeeded()
|
||||
executePendingSearchIfNeeded()
|
||||
emitDemoState()
|
||||
}
|
||||
|
||||
public func epubReader(_ reader: UIViewController, didChangeSelection selection: RDEPUBSelection?) {
|
||||
emitDemoState(prefix: "selection=\(selection == nil ? 0 : 1)")
|
||||
}
|
||||
|
||||
public func epubReader(_ reader: UIViewController, didUpdateHighlights highlights: [RDEPUBHighlight]) {
|
||||
emitDemoState(prefix: "highlights=\(highlights.count)")
|
||||
}
|
||||
|
||||
public func epubReader(_ reader: UIViewController, didUpdateBookmarks bookmarks: [RDEPUBBookmark]) {
|
||||
emitDemoState(prefix: "bookmarks=\(bookmarks.count)")
|
||||
}
|
||||
|
||||
public func epubReader(_ reader: UIViewController, didActivateExternalLink url: URL) {
|
||||
externalLinkActivationCount += 1
|
||||
lastActivatedExternalURL = url
|
||||
emitDemoState(prefix: "externalLinks=\(externalLinkActivationCount)")
|
||||
}
|
||||
|
||||
public func epubReader(_ reader: UIViewController, didFailWithError error: Error) {
|
||||
lastReaderErrorDescription = String(describing: error)
|
||||
emitDemoState(prefix: "lastError=\(lastReaderErrorDescription)")
|
||||
}
|
||||
}
|
||||
|
||||
private extension RDEpubReaderView.DisplayType {
|
||||
|
||||
var demoArgumentValue: String {
|
||||
switch self {
|
||||
case .pageCurl:
|
||||
return "pageCurl"
|
||||
case .horizontalScroll:
|
||||
return "horizontalScroll"
|
||||
case .verticalScroll:
|
||||
return "verticalScroll"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user