基础epub阅读功能

This commit is contained in:
DelLevin-Home
2026-06-27 11:20:27 +08:00
parent 02242089e8
commit 5acbc3a602
18 changed files with 980 additions and 330 deletions

View File

@@ -0,0 +1,121 @@
import 'package:flutter/material.dart';
import '../../../models/book_annotation.dart';
import '../../../utils/reader/book_annotation_dao.dart';
/// 书签列表组件 — 显示当前书籍的所有书签,点击跳转
class BookmarkList extends StatefulWidget {
final String bookId;
final void Function(String cfi) onNavigate;
const BookmarkList({
super.key,
required this.bookId,
required this.onNavigate,
});
@override
State<BookmarkList> createState() => _BookmarkListState();
}
class _BookmarkListState extends State<BookmarkList> {
final _dao = BookAnnotationDao();
List<BookAnnotation> _bookmarks = [];
bool _loading = true;
@override
void initState() {
super.initState();
_loadBookmarks();
}
Future<void> _loadBookmarks() async {
final bookmarks = await _dao.getBookmarks(widget.bookId);
if (mounted) {
setState(() {
_bookmarks = bookmarks;
_loading = false;
});
}
}
Future<void> _deleteBookmark(BookAnnotation bookmark) async {
await _dao.deleteById(bookmark.id!);
await _loadBookmarks();
}
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
if (_loading) {
return const Center(child: CircularProgressIndicator());
}
if (_bookmarks.isEmpty) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.bookmark_border, size: 48, color: colors.onSurfaceVariant.withAlpha(80)),
const SizedBox(height: 12),
Text('暂无书签', style: TextStyle(color: colors.onSurfaceVariant)),
const SizedBox(height: 4),
Text(
'阅读时点击顶部 ⭐ 按钮添加书签',
style: TextStyle(fontSize: 12, color: colors.onSurfaceVariant.withAlpha(150)),
),
],
),
);
}
return ListView.builder(
padding: const EdgeInsets.symmetric(vertical: 8),
itemCount: _bookmarks.length,
itemBuilder: (context, index) {
final bm = _bookmarks[index];
return Dismissible(
key: ValueKey(bm.id),
direction: DismissDirection.endToStart,
background: Container(
alignment: Alignment.centerRight,
padding: const EdgeInsets.only(right: 16),
color: colors.error,
child: const Icon(Icons.delete, color: Colors.white),
),
confirmDismiss: (_) async {
return await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('删除书签'),
content: Text('确定删除「${bm.content.isNotEmpty ? bm.content : bm.chapter}」?'),
actions: [
TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text('取消')),
TextButton(onPressed: () => Navigator.pop(ctx, true), child: const Text('删除')),
],
),
);
},
onDismissed: (_) => _deleteBookmark(bm),
child: ListTile(
leading: Icon(Icons.bookmark, color: Colors.amber, size: 20),
title: Text(
bm.content.isNotEmpty ? bm.content : bm.chapter,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontSize: 14),
),
subtitle: Text(
bm.chapter.isNotEmpty ? bm.chapter : '',
style: TextStyle(fontSize: 12, color: colors.onSurfaceVariant),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
dense: true,
onTap: () => widget.onNavigate(bm.cfi),
),
);
},
);
}
}

View File

@@ -0,0 +1,144 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../../../models/book_annotation.dart';
import '../../../utils/reader/book_annotation_dao.dart';
/// 阅读笔记面板 — 显示当前书籍的高亮和下划线批注
class ReadingNotesPanel extends StatefulWidget {
final String bookId;
final void Function(String cfi)? onNavigate;
const ReadingNotesPanel({
super.key,
required this.bookId,
this.onNavigate,
});
@override
State<ReadingNotesPanel> createState() => _ReadingNotesPanelState();
}
class _ReadingNotesPanelState extends State<ReadingNotesPanel> {
final _dao = BookAnnotationDao();
List<BookAnnotation> _annotations = [];
bool _loading = true;
@override
void initState() {
super.initState();
_loadAnnotations();
}
Future<void> _loadAnnotations() async {
final list = await _dao.getAnnotations(widget.bookId);
if (mounted) {
setState(() {
_annotations = list;
_loading = false;
});
}
}
Future<void> _deleteAnnotation(BookAnnotation anno) async {
await _dao.deleteById(anno.id!);
await _loadAnnotations();
}
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
if (_loading) {
return const Padding(
padding: EdgeInsets.all(24),
child: Center(child: CircularProgressIndicator()),
);
}
if (_annotations.isEmpty) {
return Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.edit_note, size: 40, color: colors.onSurfaceVariant.withAlpha(80)),
const SizedBox(height: 12),
Text('暂无笔记', style: TextStyle(fontSize: 14, color: colors.onSurfaceVariant)),
const SizedBox(height: 4),
Text(
'选中文字后可添加高亮、下划线和笔记',
style: TextStyle(fontSize: 12, color: colors.onSurfaceVariant.withAlpha(150)),
),
],
),
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Text(
'笔记 (${_annotations.length})',
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: colors.onSurface),
),
),
...List.generate(_annotations.length, (i) {
final anno = _annotations[i];
final annoColor = Color(int.parse('0x${anno.color}'));
final isHighlight = anno.type == 'highlight';
return ListTile(
dense: true,
leading: Container(
width: 4,
height: 32,
decoration: BoxDecoration(
color: annoColor.withAlpha(180),
borderRadius: BorderRadius.circular(2),
),
),
title: Text(
anno.content,
maxLines: 3,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 13,
decoration: isHighlight ? null : TextDecoration.underline,
decorationColor: annoColor.withAlpha(120),
decorationThickness: 2,
),
),
subtitle: anno.chapter.isNotEmpty
? Text(anno.chapter,
style: TextStyle(fontSize: 11, color: colors.onSurfaceVariant),
maxLines: 1,
overflow: TextOverflow.ellipsis)
: null,
trailing: PopupMenuButton<String>(
icon: Icon(Icons.more_vert, size: 18, color: colors.onSurfaceVariant),
onSelected: (action) async {
if (action == 'copy') {
await Clipboard.setData(ClipboardData(text: anno.content));
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('已复制'), duration: Duration(seconds: 1)),
);
}
} else if (action == 'delete') {
await _deleteAnnotation(anno);
}
},
itemBuilder: (_) => [
const PopupMenuItem(value: 'copy', child: Text('复制')),
const PopupMenuItem(value: 'delete', child: Text('删除')),
],
),
onTap: widget.onNavigate != null ? () => widget.onNavigate!(anno.cfi) : null,
);
}),
],
);
}
}