feat: add in-reader search and restructure documentation
- Add RDEPUBReaderSearchBarView with animated show/hide, keyword navigation, and match counting integrated into the reader controller - Restructure docs: replace scattered design docs with consolidated BUSINESS_LOGIC.md and UML_CLASS_DIAGRAMS.md; update ARCHITECTURE.md - Add SearchTests and FanrenParseTimeTest; enhance LargeBookOnDemandTests - Add scripts/run_ui_regression.sh and summarize_ui_results.py for automated UI test execution and reporting Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
d20196ee34
commit
0e7c0577e3
@@ -155,6 +155,9 @@ public final class RDEPUBReaderController: UIViewController {
|
||||
}
|
||||
lazy var topToolView = runtime.makeTopToolView()
|
||||
lazy var bottomToolView = runtime.makeBottomToolView()
|
||||
lazy var searchBarView = RDEPUBReaderSearchBarView()
|
||||
/// 搜索栏是否当前可见
|
||||
private(set) var isSearchBarVisible = false
|
||||
var currentBookIdentifier: String? {
|
||||
get { readerContext.currentBookIdentifier }
|
||||
set { readerContext.currentBookIdentifier = newValue }
|
||||
@@ -273,6 +276,9 @@ public final class RDEPUBReaderController: UIViewController {
|
||||
currentBrightness = currentBrightness
|
||||
readerAssemblyCoordinator.assembleInterface()
|
||||
readerAssemblyCoordinator.finishExternalTextBookLaunchIfNeeded()
|
||||
readerView.onToolViewVisibilityChanged = { [weak self] isVisible in
|
||||
self?.handleToolViewVisibilityChanged(isVisible: isVisible)
|
||||
}
|
||||
}
|
||||
|
||||
public override func viewWillAppear(_ animated: Bool) {
|
||||
@@ -302,4 +308,100 @@ public final class RDEPUBReaderController: UIViewController {
|
||||
runtime.viewportMonitor.viewWillTransition(with: coordinator)
|
||||
}
|
||||
|
||||
// MARK: - 搜索栏管理
|
||||
|
||||
/// 显示搜索栏,将其添加到 readerView 并滑入动画
|
||||
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()
|
||||
}
|
||||
searchBarView.onSearchPrevious = { [weak self] in
|
||||
_ = self?.runtime.searchPrevious()
|
||||
self?.updateSearchCount()
|
||||
}
|
||||
searchBarView.onSearchNext = { [weak self] in
|
||||
_ = self?.runtime.searchNext()
|
||||
self?.updateSearchCount()
|
||||
}
|
||||
searchBarView.onClose = { [weak self] in
|
||||
self?.hideSearchBar(clearSearch: true)
|
||||
}
|
||||
|
||||
if let keyword = searchState?.keyword, !keyword.isEmpty {
|
||||
searchBarView.restoreKeyword(keyword)
|
||||
updateSearchCount()
|
||||
}
|
||||
|
||||
// 延迟到下一帧确保视图已布局后再获取焦点
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.searchBarView.textField.becomeFirstResponder()
|
||||
}
|
||||
}
|
||||
|
||||
/// 隐藏搜索栏,带动画滑出
|
||||
/// - Parameter clearSearch: 是否同时清除搜索状态
|
||||
func hideSearchBar(clearSearch: Bool = false) {
|
||||
guard isSearchBarVisible else { return }
|
||||
isSearchBarVisible = false
|
||||
|
||||
searchBarView.textField.resignFirstResponder()
|
||||
UIView.animate(withDuration: 0.3, animations: {
|
||||
self.searchBarView.transform = CGAffineTransform(translationX: 0, y: -52)
|
||||
}) { _ in
|
||||
self.searchBarView.removeFromSuperview()
|
||||
self.searchBarView.transform = .identity
|
||||
self.readerView.searchBarView = nil
|
||||
}
|
||||
|
||||
if clearSearch {
|
||||
runtime.clearSearch()
|
||||
}
|
||||
}
|
||||
|
||||
/// 同步搜索栏匹配计数
|
||||
private func updateSearchCount() {
|
||||
guard let searchState else {
|
||||
searchBarView.showNoResults()
|
||||
return
|
||||
}
|
||||
if let index = searchState.currentMatchIndex {
|
||||
searchBarView.updateMatchCount(current: index + 1, total: searchState.matches.count)
|
||||
} else if searchState.matches.isEmpty {
|
||||
searchBarView.showNoResults()
|
||||
}
|
||||
}
|
||||
|
||||
/// 当工具栏可见性变化时同步搜索栏(由 RDReaderView 回调调用)
|
||||
func handleToolViewVisibilityChanged(isVisible: Bool) {
|
||||
if isVisible {
|
||||
if searchState != nil && !isSearchBarVisible {
|
||||
showSearchBar()
|
||||
}
|
||||
} else {
|
||||
if isSearchBarVisible {
|
||||
hideSearchBar(clearSearch: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user