Reorganize docs and update reader search flows
This commit is contained in:
@@ -202,7 +202,6 @@ extension RDEPUBReaderController: RDEPUBTextContentViewDelegate {
|
||||
}
|
||||
|
||||
guard let textBook,
|
||||
let publication,
|
||||
let page = textBook.page(at: pageNumber) else {
|
||||
return nil
|
||||
}
|
||||
@@ -211,11 +210,14 @@ extension RDEPUBReaderController: RDEPUBTextContentViewDelegate {
|
||||
?? textBook.location(forPageNumber: pageNumber, bookIdentifier: currentBookIdentifier)
|
||||
guard let location else { return nil }
|
||||
|
||||
return publication.resourceResolver.normalizedLocation(
|
||||
location,
|
||||
relativeToSpineIndex: nil,
|
||||
bookIdentifier: currentBookIdentifier
|
||||
) ?? location
|
||||
if let publication {
|
||||
return publication.resourceResolver.normalizedLocation(
|
||||
location,
|
||||
relativeToSpineIndex: nil,
|
||||
bookIdentifier: currentBookIdentifier
|
||||
) ?? location
|
||||
}
|
||||
return location
|
||||
}
|
||||
|
||||
/// 同步文本阅读状态到阅读会话(页码、位置、spine、章节等)
|
||||
|
||||
@@ -310,27 +310,13 @@ public final class RDEPUBReaderController: UIViewController {
|
||||
|
||||
// MARK: - 搜索栏管理
|
||||
|
||||
/// 显示搜索栏,将其添加到 readerView 并滑入动画
|
||||
/// 显示搜索栏。工具栏可见时立即添加到视图层级并滑入动画;否则仅标记状态,
|
||||
/// 等待 `handleToolViewVisibilityChanged` 在工具栏显示时再安装。
|
||||
func showSearchBar() {
|
||||
guard !isSearchBarVisible else { return }
|
||||
isSearchBarVisible = true
|
||||
searchBarView.apply(theme: configuration.theme)
|
||||
|
||||
readerView.addSubview(searchBarView)
|
||||
readerView.searchBarView = searchBarView
|
||||
let topToolbarHeight: CGFloat = readerView.safeAreaInsets.top + 52
|
||||
NSLayoutConstraint.activate([
|
||||
searchBarView.leadingAnchor.constraint(equalTo: readerView.leadingAnchor),
|
||||
searchBarView.trailingAnchor.constraint(equalTo: readerView.trailingAnchor),
|
||||
searchBarView.topAnchor.constraint(equalTo: readerView.topAnchor, constant: topToolbarHeight),
|
||||
searchBarView.heightAnchor.constraint(equalToConstant: 52)
|
||||
])
|
||||
|
||||
searchBarView.transform = CGAffineTransform(translationX: 0, y: -52)
|
||||
UIView.animate(withDuration: 0.3) {
|
||||
self.searchBarView.transform = .identity
|
||||
}
|
||||
|
||||
searchBarView.onSearchSubmit = { [weak self] keyword in
|
||||
self?.runtime.search(keyword: keyword)
|
||||
self?.updateSearchCount()
|
||||
@@ -347,12 +333,36 @@ public final class RDEPUBReaderController: UIViewController {
|
||||
self?.hideSearchBar(clearSearch: true)
|
||||
}
|
||||
|
||||
if readerView.isShowToolView {
|
||||
installSearchBarView()
|
||||
}
|
||||
}
|
||||
|
||||
/// 将搜索栏添加到 readerView 并执行滑入动画
|
||||
private func installSearchBarView() {
|
||||
guard searchBarView.superview == nil else { return }
|
||||
searchBarView.apply(theme: configuration.theme)
|
||||
|
||||
readerView.addSubview(searchBarView)
|
||||
readerView.searchBarView = searchBarView
|
||||
let topToolbarHeight: CGFloat = readerView.safeAreaInsets.top + 52
|
||||
NSLayoutConstraint.activate([
|
||||
searchBarView.leadingAnchor.constraint(equalTo: readerView.leadingAnchor),
|
||||
searchBarView.trailingAnchor.constraint(equalTo: readerView.trailingAnchor),
|
||||
searchBarView.topAnchor.constraint(equalTo: readerView.topAnchor, constant: topToolbarHeight),
|
||||
searchBarView.heightAnchor.constraint(equalToConstant: 52)
|
||||
])
|
||||
|
||||
searchBarView.transform = CGAffineTransform(translationX: 0, y: -52)
|
||||
UIView.animate(withDuration: 0.3) {
|
||||
self.searchBarView.transform = .identity
|
||||
}
|
||||
|
||||
if let keyword = searchState?.keyword, !keyword.isEmpty {
|
||||
searchBarView.restoreKeyword(keyword)
|
||||
updateSearchCount()
|
||||
}
|
||||
|
||||
// 延迟到下一帧确保视图已布局后再获取焦点
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.searchBarView.textField.becomeFirstResponder()
|
||||
}
|
||||
@@ -379,7 +389,7 @@ public final class RDEPUBReaderController: UIViewController {
|
||||
}
|
||||
|
||||
/// 同步搜索栏匹配计数
|
||||
private func updateSearchCount() {
|
||||
func updateSearchCount() {
|
||||
guard let searchState else {
|
||||
searchBarView.showNoResults()
|
||||
return
|
||||
@@ -394,7 +404,9 @@ public final class RDEPUBReaderController: UIViewController {
|
||||
/// 当工具栏可见性变化时同步搜索栏(由 RDReaderView 回调调用)
|
||||
func handleToolViewVisibilityChanged(isVisible: Bool) {
|
||||
if isVisible {
|
||||
if searchState != nil && !isSearchBarVisible {
|
||||
if isSearchBarVisible {
|
||||
installSearchBarView()
|
||||
} else if searchState != nil {
|
||||
showSearchBar()
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -53,6 +53,7 @@ public final class RDURLReaderController: UIViewController {
|
||||
private let pendingDemoPageRetryDelay: TimeInterval = 0.25
|
||||
private var demoStateTimer: Timer?
|
||||
private var lastEmittedDemoState = ""
|
||||
private var pendingSearchKeyword: String?
|
||||
|
||||
/// 初始化方法
|
||||
/// - Parameters:
|
||||
@@ -147,18 +148,41 @@ public final class RDURLReaderController: UIViewController {
|
||||
}
|
||||
|
||||
/// 执行搜索(Demo 用)
|
||||
/// 显示搜索栏并提交关键词
|
||||
/// 显示搜索栏并提交关键词。若阅读器尚未完成加载则延迟到 `didUpdateLocation` 后执行。
|
||||
/// - Parameter keyword: 搜索关键词
|
||||
public func performDemoSearch(keyword: String) {
|
||||
guard let readerController else { return }
|
||||
readerController.showSearchBar()
|
||||
// 等搜索栏动画完成后提交搜索
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
if readerController.parser != nil || readerController.textBook != nil {
|
||||
submitSearchAfterDelay(keyword: keyword, retries: 5)
|
||||
} 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()
|
||||
if retries > 0,
|
||||
readerController.searchState?.matches.isEmpty != false,
|
||||
readerController.parser == nil {
|
||||
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: 5)
|
||||
}
|
||||
|
||||
/// 嵌入阅读器控制器到当前视图层级
|
||||
/// 根据文件类型选择合适的阅读器控制器,并通过 Child View Controller 方式嵌入
|
||||
private func embedReaderController() {
|
||||
@@ -339,6 +363,7 @@ public final class RDURLReaderController: UIViewController {
|
||||
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)
|
||||
@@ -351,6 +376,7 @@ public final class RDURLReaderController: UIViewController {
|
||||
"display=\(display)",
|
||||
"toolbar=\(toolbar)",
|
||||
"highlights=\(highlights)",
|
||||
"bookmarks=\(bookmarks)",
|
||||
"selection=\(selection)",
|
||||
"href=\(href)",
|
||||
"progression=\(progression)",
|
||||
@@ -439,11 +465,13 @@ public final class RDURLReaderController: UIViewController {
|
||||
extension RDURLReaderController: RDEPUBReaderDelegate {
|
||||
public func epubReader(_ reader: UIViewController, didOpen publication: RDEPUBPublication) {
|
||||
retryPendingDemoPageIfNeeded()
|
||||
executePendingSearchIfNeeded()
|
||||
emitDemoState()
|
||||
}
|
||||
|
||||
public func epubReader(_ reader: UIViewController, didUpdateLocation location: RDEPUBLocation) {
|
||||
retryPendingDemoPageIfNeeded()
|
||||
executePendingSearchIfNeeded()
|
||||
emitDemoState()
|
||||
}
|
||||
|
||||
@@ -454,6 +482,10 @@ extension RDURLReaderController: RDEPUBReaderDelegate {
|
||||
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)")
|
||||
}
|
||||
}
|
||||
|
||||
/// 翻页模式扩展:提供 Demo 命令行参数值
|
||||
|
||||
@@ -53,8 +53,20 @@ final class RDEPUBReaderLocationCoordinator {
|
||||
let readerView = context.readerView else {
|
||||
return nil
|
||||
}
|
||||
let pageNumber = readerView.currentPage + 1
|
||||
if (context.textBook != nil || context.bookPageMap != nil), readerView.currentPage >= 0 {
|
||||
return controller.resolvedTextLocation(forPageNumber: readerView.currentPage + 1)
|
||||
if let location = controller.resolvedTextLocation(forPageNumber: pageNumber) {
|
||||
return location
|
||||
}
|
||||
// resolvedTextLocation 可能因章节数据未加载而返回 nil,
|
||||
// 通过 readingSession 的 activePages 构建回退位置
|
||||
if let readingSession = context.readingSession,
|
||||
readingSession.activePages.indices.contains(readerView.currentPage) {
|
||||
return readingSession.fallbackLocation(
|
||||
for: readingSession.activePages[readerView.currentPage],
|
||||
bookIdentifier: context.currentBookIdentifier
|
||||
)
|
||||
}
|
||||
}
|
||||
return context.readingSession?.currentReadingLocation(bookIdentifier: context.currentBookIdentifier)
|
||||
}
|
||||
|
||||
@@ -103,8 +103,11 @@ final class RDEPUBReaderSearchCoordinator {
|
||||
|
||||
private func resolvedSearchMatches(for keyword: String) -> [RDEPUBSearchMatch] {
|
||||
guard let controller else { return [] }
|
||||
if let textBook = controller.textBook, let publication = controller.publication {
|
||||
return RDEPUBTextSearchEngine(textBook: textBook, publication: publication).search(keyword: keyword)
|
||||
if let textBook = controller.textBook {
|
||||
if let publication = controller.publication {
|
||||
return RDEPUBTextSearchEngine(textBook: textBook, publication: publication).search(keyword: keyword)
|
||||
}
|
||||
return RDEPUBTextSearchEngine.searchWithoutPublication(textBook: textBook, keyword: keyword)
|
||||
}
|
||||
if let parser = controller.parser, let publication = controller.publication {
|
||||
return RDEPUBHTMLSearchEngine(parser: parser, publication: publication).search(keyword: keyword)
|
||||
|
||||
@@ -489,6 +489,7 @@ final class RDEPUBTextContentView: UIView, UIGestureRecognizerDelegate {
|
||||
|
||||
@objc private func handleLongPress(_ gesture: UILongPressGestureRecognizer) {
|
||||
#if canImport(DTCoreText)
|
||||
layoutIfNeeded()
|
||||
selectionController.handleLongPress(
|
||||
gesture,
|
||||
renderView: coreTextRenderView,
|
||||
|
||||
@@ -60,10 +60,8 @@ final class RDEPUBTextSelectionController: NSObject {
|
||||
switch gesture.state {
|
||||
case .began, .changed:
|
||||
updateSelection(at: point, renderView: renderView, interactionController: interactionController)
|
||||
case .ended:
|
||||
case .ended, .cancelled, .failed:
|
||||
isSelecting = false
|
||||
case .cancelled, .failed:
|
||||
clearSelection(renderView: renderView)
|
||||
default:
|
||||
break
|
||||
}
|
||||
@@ -89,7 +87,9 @@ final class RDEPUBTextSelectionController: NSObject {
|
||||
interactionController: RDEPUBPageInteractionController
|
||||
) {
|
||||
guard let index = interactionController.characterIndexForViewPoint(at: point, in: renderView) else {
|
||||
clearSelection(renderView: renderView)
|
||||
if !isSelecting {
|
||||
clearSelection(renderView: renderView)
|
||||
}
|
||||
return
|
||||
}
|
||||
selectionStartIndex = index
|
||||
|
||||
Reference in New Issue
Block a user