epub阅读添加搜索

This commit is contained in:
DelLevin-Home
2026-07-01 05:03:58 +08:00
parent 0af1af7f1a
commit d5bec0fe9b
7 changed files with 573 additions and 9 deletions

View File

@@ -18,6 +18,7 @@ import 'control_panel.dart';
import 'toc_drawer.dart';
import 'image_viewer.dart';
import 'footnote_popup.dart';
import 'search_sheet.dart';
part 'mixins/spine_navigation_mixin.dart';
part 'mixins/page_navigation_mixin.dart';
@@ -94,6 +95,9 @@ class _ReaderScreenState extends State<ReaderScreen>
String displayProgress = '';
@override
Timer? progressDebouncer;
final Map<int, int> _chapterPageCounts = {};
@override
Map<int, int> get chapterPageCounts => _chapterPageCounts;
// Theme state (used by _ThemeMixin)
@override
@@ -322,18 +326,21 @@ class _ReaderScreenState extends State<ReaderScreen>
if (mounted) ToastUtil.show(context, '已移除书签');
} else {
// 添加书签
final chapterTitle = bookSession.spine.isNotEmpty &&
currentSpineItemIndex < bookSession.spine.length
? bookSession.spine[currentSpineItemIndex].href
: '';
// 尝试从 TOC 找更友好的标题
String title = chapterTitle;
String title = '';
for (final toc in bookSession.toc) {
if (toc.spineIndex == currentSpineItemIndex) {
title = toc.label;
break;
}
}
// 如果 TOC 没有标题(或者标题是原始文件路径),用页内文字内容
if (title.isEmpty || title.contains('.htm') || title.contains('.xhtml')) {
title = await _getPageTextPreview();
if (title.isEmpty) {
title = '${currentSpineItemIndex + 1}';
}
}
await _readerDao.insertBookmark({
'book_id': widget.bookId,
@@ -348,6 +355,21 @@ class _ReaderScreenState extends State<ReaderScreen>
await _loadBookmarks();
}
/// 获取当前页的文字预览(前 20 个字符)
Future<String> _getPageTextPreview() async {
try {
final result = await rendererController.webViewController?.runJavaScriptReturningResult(
"(function(){var f=document.getElementById('frame-curr');if(!f||!f.contentDocument)return '';var t=f.contentDocument.body.textContent||'';return t.trim().substring(0,20)})();"
);
if (result != null) {
final s = result.toString();
return (s.startsWith('"') && s.endsWith('"') && s.length >= 2
? s.substring(1, s.length - 1) : s).trim();
}
} catch (_) {}
return '';
}
void _jumpToBookmark(Map<String, dynamic> bookmark) {
final cfi = bookmark['cfi'] as String? ?? '';
if (cfi.isEmpty) return;
@@ -384,6 +406,23 @@ class _ReaderScreenState extends State<ReaderScreen>
scaffoldKey.currentState?.openDrawer();
}
void _openSearch() {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (ctx) => SearchSheet(
bookSession: bookSession,
streamService: _streamService,
onNavigate: (spineIndex, keyword, scrollRatio) {
Navigator.pop(ctx);
setState(() => currentSpineItemIndex = spineIndex);
loadCarousel(restoreScrollRatio: scrollRatio);
},
),
);
}
@override
Widget build(BuildContext context) {
if (!bookSession.isLoaded) {
@@ -602,6 +641,7 @@ class _ReaderScreenState extends State<ReaderScreen>
},
currentPageHasBookmark: _currentPageHasBookmark,
onBookmarkToggle: _toggleBookmark,
onSearchTap: _openSearch,
),
],
),