Reorganize docs and update reader search flows

This commit is contained in:
shenlei
2026-06-10 08:22:07 +08:00
parent 0e7c0577e3
commit d15187b730
72 changed files with 517 additions and 5214 deletions
@@ -37,9 +37,9 @@ public struct RDEPUBSelection: Codable, Equatable {
self.createdAt = createdAt
}
///
///
public var isEmpty: Bool {
text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || rangeInfo?.isEmpty != false
text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
}
}
@@ -81,4 +81,51 @@ final class RDEPUBTextSearchEngine: RDEPUBSearchEngine {
let range = NSRange(location: start, length: max(end - start, 0))
return text.substring(with: range).trimmingCharacters(in: .whitespacesAndNewlines)
}
/// publication txt
static func searchWithoutPublication(textBook: RDEPUBTextBook, keyword: String) -> [RDEPUBSearchMatch] {
let normalizedKeyword = keyword.trimmingCharacters(in: .whitespacesAndNewlines)
guard !normalizedKeyword.isEmpty else { return [] }
var matches: [RDEPUBSearchMatch] = []
for chapter in textBook.chapters {
let source = chapter.attributedContent.string as NSString
let fullLength = source.length
guard fullLength > 0 else { continue }
var localMatchIndex = 0
var searchRange = NSRange(location: 0, length: fullLength)
while searchRange.length > 0 {
let foundRange = source.range(of: normalizedKeyword, options: [.caseInsensitive], range: searchRange)
guard foundRange.location != NSNotFound else { break }
let progressionDenominator = max(fullLength - 1, 1)
let progression = Double(foundRange.location) / Double(progressionDenominator)
let previewRadius = 12
let previewStart = max(foundRange.location - previewRadius, 0)
let previewEnd = min(foundRange.location + foundRange.length + previewRadius, fullLength)
let previewRange = NSRange(location: previewStart, length: max(previewEnd - previewStart, 0))
let previewText = source.substring(with: previewRange).trimmingCharacters(in: .whitespacesAndNewlines)
matches.append(
RDEPUBSearchMatch(
href: chapter.href,
progression: progression,
previewText: previewText,
localMatchIndex: localMatchIndex,
rangeLocation: foundRange.location,
rangeLength: foundRange.length,
rangeAnchor: nil
)
)
localMatchIndex += 1
let nextLocation = foundRange.location + max(foundRange.length, 1)
if nextLocation >= fullLength { break }
searchRange = NSRange(location: nextLocation, length: fullLength - nextLocation)
}
}
return matches
}
}
@@ -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