fix epub reader playback and trial navigation

This commit is contained in:
shen
2026-07-11 08:16:56 +08:00
parent d7fcda345d
commit 063a493b18
231 changed files with 435 additions and 36060 deletions
@@ -49,14 +49,24 @@ final class RDEPUBReaderChapterListController: UITableViewController {
cell.backgroundColor = theme.contentBackgroundColor
cell.textLabel?.numberOfLines = 2
cell.textLabel?.text = item.title
cell.textLabel?.textColor = isCurrentItem(item) ? .systemBlue : theme.contentTextColor
if isCurrentItem(item) {
cell.textLabel?.textColor = .systemBlue
} else if item.isReadable {
cell.textLabel?.textColor = theme.contentTextColor
} else {
cell.textLabel?.textColor = .systemGray
}
cell.selectionStyle = item.isReadable ? .default : .none
cell.isUserInteractionEnabled = item.isReadable
cell.indentationLevel = item.depth
cell.indentationWidth = 18
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
onSelectItem?(items[indexPath.row])
let item = items[indexPath.row]
guard item.isReadable else { return }
onSelectItem?(item)
}
private func isCurrentItem(_ item: RDEPUBReaderTableOfContentsItem) -> Bool {
@@ -15,6 +15,18 @@ extension RDEPUBReaderController: RDEpubReaderDataSource, RDEpubReaderPageProvid
pageIdentifier(readerView: readerView, pageNum: index)
}
public func readerView(
_ readerView: RDEpubReaderView,
shouldPreloadPageAt index: Int
) -> Bool {
// 宿 UIView
guard index != trialWallPageIndex else { return false }
// /
//
//
return request(for: index)?.isFixedLayout != true
}
public func readerViewTopChrome(_ readerView: RDEpubReaderView) -> UIView? {
topToolView(readerView: readerView)
}
@@ -66,6 +78,7 @@ extension RDEPUBReaderController: RDEpubReaderDataSource, RDEpubReaderPageProvid
pageNumber: pageNum + 1,
totalPages: pageCountOfReaderView(readerView: readerView),
configuration: configuration,
contentInsets: readerContext.currentTextContentInsets(pageIndex: pageNum),
chapterCFIMap: resolvedPage.chapter.chapterOffsetMap.cfiMap,
chapterFragmentOffsets: resolvedPage.chapter.chapterOffsetMap.fragmentOffsets,
highlights: textHighlights(for: resolvedPage.page),
@@ -89,7 +102,8 @@ extension RDEPUBReaderController: RDEpubReaderDataSource, RDEpubReaderPageProvid
contentView.configureLoading(
pageNumber: pageNum + 1,
totalPages: pageCountOfReaderView(readerView: readerView),
configuration: configuration
configuration: configuration,
contentInsets: readerContext.currentTextContentInsets(pageIndex: pageNum)
)
return contentView
}
@@ -111,6 +125,7 @@ extension RDEPUBReaderController: RDEpubReaderDataSource, RDEpubReaderPageProvid
pageNumber: pageNum + 1,
totalPages: textBook.pages.count,
configuration: configuration,
contentInsets: readerContext.currentTextContentInsets(pageIndex: pageNum),
chapterCFIMap: textBook.chapterData(for: page.href)?.chapter.cfiMap,
chapterFragmentOffsets: textBook.chapterData(for: page.href)?.chapter.fragmentOffsets ?? [:],
highlights: textHighlights(for: page),
@@ -48,12 +48,7 @@ extension RDEPUBReaderController {
forPageNumber: pageIndex + 1,
spineIndex: page.spineIndex
)
var preferences = currentPreferences()
if publication.layout == .fixed, readerView.pagesPerScreen == 2 {
// 5pt 10pt
preferences.fixedContentInset.left += 5
preferences.fixedContentInset.right += 5
}
let preferences = readerContext.currentPreferences(pageIndex: pageIndex)
return preferences.renderRequest(
for: page,
publication: publication,
@@ -50,12 +50,14 @@ extension RDEPUBReaderController {
items.flatMap { item in
let location = RDEPUBLocation(bookIdentifier: currentBookIdentifier, href: item.href, progression: 0)
let pageNumber = includePageNumbers ? resolvedTableOfContentsPageNumber(for: location) : nil
let isReadable = isReadableTableOfContentsLocation(location)
let current = RDEPUBReaderTableOfContentsItem(
title: item.title,
href: item.href,
depth: depth,
pageNumber: pageNumber
pageNumber: pageNumber,
isReadable: isReadable
)
return [current] + flattenedTableOfContentsItems(
from: item.children,
@@ -56,6 +56,17 @@ extension RDEPUBReaderController {
return readableContentPageCount + 1
}
func isReadableTableOfContentsLocation(_ location: RDEPUBLocation) -> Bool {
guard let policy = configuration.trialPolicy,
let readableChapterCount = policy.readableChapterCount else {
return true
}
guard let spineIndex = readerContext.normalizedSpineIndex(for: location) else {
return true
}
return spineIndex < readableChapterCount
}
/// 宿
func makeTrialWallPageView(reusableView: UIView?) -> UIView {
let container = (reusableView as? RDEPUBTrialWallContainerView) ?? RDEPUBTrialWallContainerView()
@@ -65,12 +76,19 @@ extension RDEPUBReaderController {
}
/// 宿
final class RDEPUBTrialWallContainerView: UIView {
final class RDEPUBTrialWallContainerView: UIView, RDEpubReaderCachePolicyProviding {
/// 宿 View
/// View
var shouldAvoidReaderPageCaching: Bool { true }
private weak var wallView: UIView?
func setWallView(_ view: UIView?) {
guard wallView !== view else { return }
// 使 View
if wallView === view, view?.superview === self {
return
}
wallView?.removeFromSuperview()
guard let view else { return }
view.translatesAutoresizingMaskIntoConstraints = false
@@ -149,6 +149,10 @@ public final class RDEPUBReaderController: UIViewController {
/// delegate RDEPUBReaderController+Trial
var cachedTrialWallView: UIView?
/// 宿使
/// 退
private var navigationBarHiddenBeforeReader: Bool?
private(set) var isSearchBarVisible = false
var currentBookIdentifier: String? {
@@ -275,6 +279,9 @@ public final class RDEPUBReaderController: UIViewController {
public override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if navigationBarHiddenBeforeReader == nil {
navigationBarHiddenBeforeReader = navigationController?.isNavigationBarHidden
}
navigationController?.setNavigationBarHidden(true, animated: animated)
navigationController?.interactivePopGestureRecognizer?.delegate = self
navigationController?.interactivePopGestureRecognizer?.isEnabled = true
@@ -282,7 +289,12 @@ public final class RDEPUBReaderController: UIViewController {
public override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
navigationController?.setNavigationBarHidden(false, animated: animated)
if let navigationBarHiddenBeforeReader {
navigationController?.setNavigationBarHidden(
navigationBarHiddenBeforeReader,
animated: animated
)
}
}
public override func viewDidAppear(_ animated: Bool) {
@@ -10,10 +10,19 @@ public struct RDEPUBReaderTableOfContentsItem: Equatable {
public var pageNumber: Int?
public init(title: String, href: String, depth: Int, pageNumber: Int? = nil) {
public var isReadable: Bool
public init(
title: String,
href: String,
depth: Int,
pageNumber: Int? = nil,
isReadable: Bool = true
) {
self.title = title
self.href = href
self.depth = depth
self.pageNumber = pageNumber
self.isReadable = isReadable
}
}
}
@@ -19,7 +19,7 @@ protocol RDEPUBWebContentViewDelegate: AnyObject {
func epubWebContentView(_ contentView: RDEPUBWebContentView, didTapFootnoteWithAltText altText: String, sourceRect: CGRect?)
}
final class RDEPUBWebContentView: UIView, RDEpubReaderPageResourceReleasing {
final class RDEPUBWebContentView: UIView, RDEpubReaderPageResourceReleasing, RDEpubReaderPageVisibilityHandling {
weak var delegate: RDEPUBWebContentViewDelegate?
/// The current href of the loaded chapter (exposed for image resolution).
@@ -90,6 +90,10 @@ final class RDEPUBWebContentView: UIView, RDEpubReaderPageResourceReleasing {
self?.decorationOverlayView.applyDecorations(decorations)
}
}
func readerPageVisibilityDidChange(isVisible: Bool) {
epubWebView.setReaderPageVisible(isVisible)
}
}
extension RDEPUBWebContentView: RDEPUBWebViewDelegate {
@@ -162,9 +162,12 @@ final class RDEPUBReaderContext {
environment.currentLayoutContext(configuration: configuration)
}
func currentPreferences() -> RDEPUBPreferences {
let safeInsets = RDEPUBSafeArea.resolve(controller?.view.safeAreaInsets)
return configuration.makePreferences(safeAreaInsets: safeInsets)
func currentPreferences(pageIndex: Int? = nil) -> RDEPUBPreferences {
environment.currentPreferences(configuration: configuration, pageIndex: pageIndex)
}
func currentTextContentInsets(pageIndex: Int) -> UIEdgeInsets {
currentPreferences(pageIndex: pageIndex).reflowableContentInsets
}
/// Captures all layout parameters needed for background chapter loading.
@@ -10,14 +10,59 @@ enum RDEPUBTextPageLayoutMetrics {
static func contentInsets(
configuration: RDEPUBReaderConfiguration,
safeAreaInsets: UIEdgeInsets
safeAreaInsets: UIEdgeInsets,
isLandscapeDualPage: Bool = false,
isLeadingPage: Bool = true
) -> UIEdgeInsets {
let configInsets = configuration.reflowableContentInsets
guard isLandscapeDualPage else {
return UIEdgeInsets(
top: max(configInsets.top, safeAreaInsets.top),
left: max(configInsets.left, safeAreaInsets.left),
bottom: max(configInsets.bottom, safeAreaInsets.bottom) + pageNumberReservedHeight,
right: max(configInsets.right, safeAreaInsets.right)
)
}
let outerHorizontalInset = max(
max(configInsets.left, configInsets.right),
max(safeAreaInsets.left, safeAreaInsets.right)
)
let spineInset = RDEpubReaderView.landscapeDualPageGap / 2
return UIEdgeInsets(
top: max(configInsets.top, safeAreaInsets.top),
left: max(configInsets.left, safeAreaInsets.left),
bottom: max(configInsets.bottom, safeAreaInsets.bottom) + pageNumberReservedHeight,
right: max(configInsets.right, safeAreaInsets.right)
top: max(safeAreaInsets.top, min(configInsets.top, RDEPUBSafeArea.landscapeVerticalTextMargin)),
left: isLeadingPage ? outerHorizontalInset : spineInset,
bottom: max(safeAreaInsets.bottom, min(configInsets.bottom, RDEPUBSafeArea.landscapeVerticalTextMargin)) + pageNumberReservedHeight,
right: isLeadingPage ? spineInset : outerHorizontalInset
)
}
static func fixedContentInsets(
configuration: RDEPUBReaderConfiguration,
safeAreaInsets: UIEdgeInsets,
isLandscapeDualPage: Bool,
isLeadingPage: Bool
) -> UIEdgeInsets {
let configInsets = configuration.fixedContentInset
guard isLandscapeDualPage else {
return UIEdgeInsets(
top: max(configInsets.top, safeAreaInsets.top),
left: max(configInsets.left, safeAreaInsets.left),
bottom: max(configInsets.bottom, safeAreaInsets.bottom),
right: max(configInsets.right, safeAreaInsets.right)
)
}
let outerHorizontalInset = max(
max(configInsets.left, configInsets.right),
max(safeAreaInsets.left, safeAreaInsets.right)
)
let spineInset = RDEpubReaderView.landscapeDualPageGap / 2
return UIEdgeInsets(
top: max(safeAreaInsets.top, min(configInsets.top, RDEPUBSafeArea.landscapeVerticalTextMargin)),
left: isLeadingPage ? outerHorizontalInset : spineInset,
bottom: max(safeAreaInsets.bottom, min(configInsets.bottom, RDEPUBSafeArea.landscapeVerticalTextMargin)),
right: isLeadingPage ? spineInset : outerHorizontalInset
)
}
}
@@ -74,7 +119,8 @@ final class RDEPUBReaderEnvironment {
frameHeight: max(pageSize.height, 1),
edgeInsets: RDEPUBTextPageLayoutMetrics.contentInsets(
configuration: configuration,
safeAreaInsets: safeAreaInsets
safeAreaInsets: safeAreaInsets,
isLandscapeDualPage: readerView?.pagesPerScreen == 2
),
numberOfColumns: configuration.numberOfColumns,
columnGap: configuration.columnGap,
@@ -87,6 +133,29 @@ final class RDEPUBReaderEnvironment {
)
}
func currentPreferences(
configuration: RDEPUBReaderConfiguration,
pageIndex: Int? = nil
) -> RDEPUBPreferences {
let safeAreaInsets = RDEPUBSafeArea.resolve(controller?.view.safeAreaInsets)
let isLandscapeDualPage = readerView?.pagesPerScreen == 2
let isLeadingPage = pageIndex.map { readerView?.isLeadingPage($0) ?? true } ?? true
var preferences = configuration.makePreferences(safeAreaInsets: safeAreaInsets)
preferences.reflowableContentInsets = RDEPUBTextPageLayoutMetrics.contentInsets(
configuration: configuration,
safeAreaInsets: safeAreaInsets,
isLandscapeDualPage: isLandscapeDualPage,
isLeadingPage: isLeadingPage
)
preferences.fixedContentInset = RDEPUBTextPageLayoutMetrics.fixedContentInsets(
configuration: configuration,
safeAreaInsets: safeAreaInsets,
isLandscapeDualPage: isLandscapeDualPage,
isLeadingPage: isLeadingPage
)
return preferences
}
var currentBrightness: CGFloat {
get { displayEnvironment.currentBrightness }
set { displayEnvironment.currentBrightness = newValue }
@@ -297,6 +297,9 @@ final class RDEPUBReaderSearchCoordinator {
private func notifySearchStateChanged() {
guard let controller else { return }
let state = controller.searchState
//
//
controller.updateSearchCount()
controller.delegate?.epubReader(controller, didUpdateSearchResult: state?.result)
controller.delegate?.epubReader(controller, didChangeCurrentSearchMatch: state?.currentMatch)
}
@@ -411,4 +414,4 @@ final class RDEPUBReaderSearchCoordinator {
return nil
}
}
}
@@ -398,6 +398,7 @@ final class RDEPUBTextContentView: UIView, UIGestureRecognizerDelegate, RDEpubRe
pageNumber: Int,
totalPages: Int,
configuration: RDEPUBReaderConfiguration,
contentInsets: UIEdgeInsets? = nil,
chapterCFIMap: RDEPUBCFIMap? = nil,
chapterFragmentOffsets: [String: Int] = [:],
highlights: [RDEPUBHighlight] = [],
@@ -414,7 +415,7 @@ final class RDEPUBTextContentView: UIView, UIGestureRecognizerDelegate, RDEpubRe
currentHighlights = highlights
currentSearchState = searchState
selectionController.clearSelection(renderView: coreTextRenderView)
contentInsets = Self.safeContentInsets(
self.contentInsets = contentInsets ?? Self.safeContentInsets(
configuration: configuration,
safeAreaInsets: RDEPUBSafeArea.resolve(safeAreaInsets)
)
@@ -491,7 +492,8 @@ final class RDEPUBTextContentView: UIView, UIGestureRecognizerDelegate, RDEpubRe
func configureLoading(
pageNumber: Int,
totalPages: Int,
configuration: RDEPUBReaderConfiguration
configuration: RDEPUBReaderConfiguration,
contentInsets: UIEdgeInsets? = nil
) {
currentPage = nil
currentChapterCFIMap = nil
@@ -502,7 +504,7 @@ final class RDEPUBTextContentView: UIView, UIGestureRecognizerDelegate, RDEpubRe
currentHighlights = []
currentSearchState = nil
selectionController.clearSelection(renderView: coreTextRenderView)
contentInsets = Self.safeContentInsets(
self.contentInsets = contentInsets ?? Self.safeContentInsets(
configuration: configuration,
safeAreaInsets: RDEPUBSafeArea.resolve(safeAreaInsets)
)