修复翻页速度过快导致高亮/摘抄失效的问题

This commit is contained in:
DelLevin-Home
2026-07-05 02:17:34 +08:00
parent e2c15cef99
commit 1529c7d7cb
7 changed files with 249 additions and 47 deletions

View File

@@ -12,6 +12,7 @@ mixin _TextSelectionMixin on State<ReaderScreen> {
BookSession get bookSession;
ReaderRendererController get rendererController;
int get currentSpineItemIndex;
Completer<void>? get pageCountReadyCompleter;
// ─── 选中工具条状态 ──────────────────────────────────────────────
String? _selectionText;
@@ -23,6 +24,10 @@ mixin _TextSelectionMixin on State<ReaderScreen> {
String? _pendingScrollXPath;
String? _pendingScrollText;
// 已有高亮/摘抄信息(选中文字时检测)
int? _existingHighlightId;
bool _existingIsExcerpt = false;
bool get isSelectionToolbarVisible =>
_selectionText != null && _selectionRect != null;
@@ -43,6 +48,8 @@ mixin _TextSelectionMixin on State<ReaderScreen> {
_dismissSelectionToolbar();
return;
}
// 检测选中文本是否已有高亮/摘抄
_detectExistingHighlight(selectedText, currentSpineItemIndex);
setState(() {
_selectionText = selectedText;
_selectionRect = rect;
@@ -53,6 +60,22 @@ mixin _TextSelectionMixin on State<ReaderScreen> {
});
}
/// 检测选中文本是否已有高亮或摘抄
Future<void> _detectExistingHighlight(String text, int spineIndex) async {
final existing = await _readerDao.getHighlightsByBookId(widget.bookId);
final match = existing.where((h) =>
h['chapter'] == spineIndex.toString() &&
h['content'] == text).toList();
if (match.isEmpty) {
_existingHighlightId = null;
_existingIsExcerpt = false;
} else {
final h = match.first;
_existingHighlightId = h['id'] as int?;
_existingIsExcerpt = h['color'] == 'excerpt';
}
}
/// 关闭工具条并清除 WebView 选区
void _dismissSelectionToolbar() {
if (!isSelectionToolbarVisible) return;
@@ -64,6 +87,8 @@ mixin _TextSelectionMixin on State<ReaderScreen> {
_startHandle = null;
_endHandle = null;
_selectionInfo = null;
_existingHighlightId = null;
_existingIsExcerpt = false;
});
}
@@ -236,6 +261,60 @@ mixin _TextSelectionMixin on State<ReaderScreen> {
_dismissSelectionToolbar();
}
// ─── 取消高亮/摘抄 ─────────────────────────────────────────────
/// 取消高亮:删除 DB 记录 + 移除 DOM 标注 → 关闭工具条
Future<void> _onRemoveHighlightButton() async {
final id = _existingHighlightId;
if (id == null) return;
final controller = _webViewControllerMixin;
await _readerDao.deleteHighlight(id);
if (controller != null) {
// 先清除浏览器选区,防止活跃选区干扰 DOM 操作
await controller.clearSelection();
await Future.delayed(const Duration(milliseconds: 50));
await controller.removeHighlight(id.toString());
}
if (!mounted) return;
ToastUtil.show(context, '已取消高亮');
_dismissSelectionToolbar();
}
/// 取消摘抄:删除 book_excerpts 记录 + 删除蓝色标注 → 关闭工具条
Future<void> _onRemoveExcerptButton() async {
final text = _selectionText;
final id = _existingHighlightId;
if (text == null || id == null) return;
final linkedBookId = bookSession.book['book_id'] as String? ?? '';
// 1. 删除 book_excerpts 中的摘抄记录
if (linkedBookId.isNotEmpty) {
final db = await DatabaseHelper.instance.database;
await db.delete(
'book_excerpts',
where: 'book_id = ? AND content = ?',
whereArgs: [linkedBookId, text],
);
}
// 2. 删除 book_annotations 中的蓝色标注
await _readerDao.deleteHighlight(id);
// 3. 移除 DOM 中的高亮标记
final controller = _webViewControllerMixin;
if (controller != null) {
// 先清除浏览器选区,防止活跃选区干扰 DOM 操作
await controller.clearSelection();
await Future.delayed(const Duration(milliseconds: 50));
await controller.removeHighlight(id.toString());
}
if (!mounted) return;
ToastUtil.show(context, '已取消摘抄');
_dismissSelectionToolbar();
}
// ─── 高亮恢复 ───────────────────────────────────────────────────
/// spine 加载完成后调用:先跳转到高亮位置,再恢复高亮
@@ -250,14 +329,24 @@ mixin _TextSelectionMixin on State<ReaderScreen> {
final text = _pendingScrollText ?? '';
_pendingScrollXPath = null;
_pendingScrollText = null;
await Future.delayed(const Duration(milliseconds: 300));
var pageIndex = await controller.getPageIndexForXPath(xpath);
debugPrint('[MN] scrollToXPath: $xpath → page $pageIndex');
// XPath 找不到时用文本搜索回退
if (pageIndex < 0 && text.isNotEmpty) {
debugPrint('[MN] XPath failed, trying text search...');
// 等待分栏布局完成onPageCountReady 触发后)
final completer = pageCountReadyCompleter;
if (completer != null && !completer.isCompleted) {
await completer.future.timeout(const Duration(seconds: 5), onTimeout: () {});
}
// 分栏完成后额外等待一帧确保布局稳定
await Future.delayed(const Duration(milliseconds: 100));
// 用文本搜索定位(比 XPath 更可靠)
int pageIndex = -1;
if (text.isNotEmpty) {
pageIndex = await controller.getPageIndexForText(text);
debugPrint('[MN] text search → page $pageIndex');
debugPrint('[MN] scrollToText: page $pageIndex');
}
if (pageIndex < 0 && xpath.isNotEmpty) {
pageIndex = await controller.getPageIndexForXPath(xpath);
debugPrint('[MN] scrollToXPath: $xpath → page $pageIndex');
}
if (pageIndex >= 0) {
await controller.jumpToPage(pageIndex);