refactor: split reader architecture and chrome handling
This commit is contained in:
@@ -0,0 +1,183 @@
|
||||
import Foundation
|
||||
|
||||
final class RDEPUBReaderSearchCoordinator {
|
||||
private unowned let context: RDEPUBReaderContext
|
||||
|
||||
init(context: RDEPUBReaderContext) {
|
||||
self.context = context
|
||||
}
|
||||
|
||||
private var controller: RDEPUBReaderController? {
|
||||
context.controller
|
||||
}
|
||||
|
||||
func search(keyword: String) {
|
||||
guard let controller else { return }
|
||||
let normalizedKeyword = keyword.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !normalizedKeyword.isEmpty else {
|
||||
clearSearch()
|
||||
return
|
||||
}
|
||||
|
||||
let matches = resolvedSearchMatches(for: normalizedKeyword)
|
||||
controller.searchState = RDEPUBSearchState(
|
||||
keyword: normalizedKeyword,
|
||||
matches: matches,
|
||||
currentMatchIndex: matches.isEmpty ? nil : 0
|
||||
)
|
||||
notifySearchStateChanged()
|
||||
|
||||
if matches.isEmpty {
|
||||
controller.refreshVisibleContentPreservingLocation()
|
||||
} else {
|
||||
_ = navigateToCurrentSearchMatch(animated: false)
|
||||
}
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func searchNext() -> Bool {
|
||||
advanceSearch(by: 1)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func searchPrevious() -> Bool {
|
||||
advanceSearch(by: -1)
|
||||
}
|
||||
|
||||
func clearSearch() {
|
||||
guard let controller else { return }
|
||||
controller.searchState = nil
|
||||
notifySearchStateChanged()
|
||||
controller.refreshVisibleContentPreservingLocation()
|
||||
}
|
||||
|
||||
func searchPresentation(for page: EPUBPage) -> RDEPUBSearchPresentation? {
|
||||
guard let controller else { return nil }
|
||||
guard let searchState = controller.searchState,
|
||||
let publication = controller.publication else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let pageHrefs: [String]
|
||||
if let fixedSpread = page.fixedSpread {
|
||||
pageHrefs = fixedSpread.resources.map { publication.resourceResolver.normalizedHref($0.href) ?? $0.href }
|
||||
} else if publication.spine.indices.contains(page.spineIndex) {
|
||||
pageHrefs = [
|
||||
publication.resourceResolver.normalizedHref(publication.spine[page.spineIndex].href)
|
||||
?? publication.spine[page.spineIndex].href
|
||||
]
|
||||
} else {
|
||||
pageHrefs = []
|
||||
}
|
||||
|
||||
guard !pageHrefs.isEmpty else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let currentMatch = searchState.currentMatch
|
||||
let normalizedCurrentHref = currentMatch.map { publication.resourceResolver.normalizedHref($0.href) ?? $0.href }
|
||||
let resources = pageHrefs.map { href in
|
||||
let matchCount = searchState.matches.filter {
|
||||
(publication.resourceResolver.normalizedHref($0.href) ?? $0.href) == href
|
||||
}.count
|
||||
let activeLocalMatchIndex = normalizedCurrentHref == href ? currentMatch?.localMatchIndex : nil
|
||||
return RDEPUBSearchPresentationResource(
|
||||
href: href,
|
||||
matchCount: matchCount,
|
||||
activeLocalMatchIndex: activeLocalMatchIndex
|
||||
)
|
||||
}
|
||||
return RDEPUBSearchPresentation(keyword: searchState.keyword, resources: resources)
|
||||
}
|
||||
|
||||
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 parser = controller.parser, let publication = controller.publication {
|
||||
return RDEPUBHTMLSearchEngine(parser: parser, publication: publication).search(keyword: keyword)
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
private func advanceSearch(by delta: Int) -> Bool {
|
||||
guard let controller else { return false }
|
||||
guard var searchState = controller.searchState, !searchState.matches.isEmpty else {
|
||||
return false
|
||||
}
|
||||
|
||||
let currentIndex = searchState.currentMatchIndex ?? 0
|
||||
let nextIndex = (currentIndex + delta + searchState.matches.count) % searchState.matches.count
|
||||
searchState.currentMatchIndex = nextIndex
|
||||
controller.searchState = searchState
|
||||
notifySearchStateChanged()
|
||||
return navigateToCurrentSearchMatch(animated: true)
|
||||
}
|
||||
|
||||
private func notifySearchStateChanged() {
|
||||
guard let controller else { return }
|
||||
controller.delegate?.epubReader(controller, didUpdateSearchResult: controller.searchState?.result)
|
||||
controller.delegate?.epubReader(controller, didChangeCurrentSearchMatch: controller.searchState?.currentMatch)
|
||||
}
|
||||
|
||||
private func navigateToCurrentSearchMatch(animated: Bool) -> Bool {
|
||||
guard let controller else { return false }
|
||||
guard let searchMatch = controller.searchState?.currentMatch else {
|
||||
controller.refreshVisibleContentPreservingLocation()
|
||||
return false
|
||||
}
|
||||
|
||||
if let targetPageNumber = pageNumber(for: searchMatch),
|
||||
controller.readerView.currentPage == targetPageNumber - 1 {
|
||||
controller.refreshVisibleContentPreservingLocation()
|
||||
return true
|
||||
}
|
||||
|
||||
let location = RDEPUBLocation(
|
||||
bookIdentifier: controller.currentBookIdentifier,
|
||||
href: searchMatch.href,
|
||||
progression: searchMatch.progression,
|
||||
lastProgression: searchMatch.progression,
|
||||
fragment: nil,
|
||||
rangeAnchor: searchMatch.rangeAnchor
|
||||
)
|
||||
return controller.restoreReadingLocation(location, animated: animated)
|
||||
}
|
||||
|
||||
private func pageNumber(for searchMatch: RDEPUBSearchMatch) -> Int? {
|
||||
guard let controller else { return nil }
|
||||
if let chapterData = controller.textChapterData(forNormalizedHref: searchMatch.href) {
|
||||
if let pageNumber = chapterData.pageNumber(for: searchMatch) {
|
||||
return pageNumber
|
||||
}
|
||||
|
||||
if let rangeLocation = searchMatch.rangeLocation,
|
||||
let page = chapterData.page(containing: rangeLocation) {
|
||||
return page.absolutePageIndex + 1
|
||||
}
|
||||
}
|
||||
|
||||
let location = RDEPUBLocation(
|
||||
bookIdentifier: controller.currentBookIdentifier,
|
||||
href: searchMatch.href,
|
||||
progression: searchMatch.progression,
|
||||
lastProgression: searchMatch.progression,
|
||||
fragment: nil,
|
||||
rangeAnchor: searchMatch.rangeAnchor
|
||||
)
|
||||
|
||||
if let textBook = controller.textBook, let publication = controller.publication {
|
||||
return textBook.pageNumber(
|
||||
for: location,
|
||||
resolver: publication.resourceResolver,
|
||||
bookIdentifier: controller.currentBookIdentifier
|
||||
)
|
||||
}
|
||||
|
||||
return controller.readingSession?.pageIndex(
|
||||
for: location,
|
||||
bookIdentifier: controller.currentBookIdentifier
|
||||
).map { $0 + 1 }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user