新增epub书摘,高亮文字,适配平板

This commit is contained in:
DelLevin-Home
2026-07-02 05:22:21 +08:00
parent a0a146854b
commit a7e4845e46
27 changed files with 3712 additions and 505 deletions

View File

@@ -3,6 +3,7 @@ import '../../utils/book/book_dao.dart';
import '../../utils/epub/reader_dao.dart';
import '../../utils/toast_util.dart';
import '../../models/data_models.dart';
import '../../widgets/fade_in_local_image.dart';
/// 选择关联书籍页面(带搜索功能)
class BookLinkPage extends StatefulWidget {
@@ -18,6 +19,7 @@ class _BookLinkPageState extends State<BookLinkPage> {
final BookDao _bookDao = BookDao();
final ReaderDao _readerDao = ReaderDao();
final TextEditingController _searchCtrl = TextEditingController();
final FocusNode _searchFocus = FocusNode();
List<Book> _allBooks = [];
List<Book> _filteredBooks = [];
@@ -33,6 +35,7 @@ class _BookLinkPageState extends State<BookLinkPage> {
@override
void dispose() {
_searchCtrl.dispose();
_searchFocus.dispose();
super.dispose();
}
@@ -88,52 +91,17 @@ class _BookLinkPageState extends State<BookLinkPage> {
body: Column(
children: [
// 搜索栏
Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 12),
child: TextField(
controller: _searchCtrl,
onChanged: _onSearch,
decoration: InputDecoration(
hintText: '搜索书名或作者...',
hintStyle: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.35)),
prefixIcon: Icon(Icons.search, size: 20, color: colors.onSurface.withValues(alpha: 0.4)),
suffixIcon: _query.isNotEmpty
? IconButton(
icon: Icon(Icons.close, size: 18, color: colors.onSurface.withValues(alpha: 0.4)),
onPressed: () {
_searchCtrl.clear();
_onSearch('');
},
)
: null,
filled: true,
fillColor: colors.surfaceContainerHighest,
contentPadding: const EdgeInsets.symmetric(vertical: 10),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
),
style: const TextStyle(fontSize: 14),
),
),
_buildSearchBar(colors),
// 书籍列表
Expanded(
child: _isLoading
? Center(child: CircularProgressIndicator(color: colors.primary))
: _filteredBooks.isEmpty
? Center(
child: Text(
_query.isEmpty ? '暂无书籍' : '未找到匹配的书籍',
style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.4)),
),
)
: ListView.separated(
? _buildEmptyState(colors)
: ListView.builder(
padding: const EdgeInsets.fromLTRB(16, 4, 16, 24),
itemCount: _filteredBooks.length,
separatorBuilder: (_, __) =>
Divider(height: 0.5, indent: 72, color: colors.outlineVariant),
itemBuilder: (_, index) => _buildBookTile(_filteredBooks[index], colors),
itemBuilder: (_, index) => _buildBookCard(_filteredBooks[index], colors, index),
),
),
],
@@ -141,46 +109,217 @@ class _BookLinkPageState extends State<BookLinkPage> {
);
}
Widget _buildBookTile(Book book, ColorScheme colors) {
// ── 搜索栏 ──
Widget _buildSearchBar(ColorScheme colors) {
return Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 12),
child: Container(
decoration: BoxDecoration(
color: colors.surfaceContainerHigh,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: colors.shadow.withValues(alpha: 0.04),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: TextField(
controller: _searchCtrl,
focusNode: _searchFocus,
onChanged: _onSearch,
style: TextStyle(fontSize: 14, color: colors.onSurface),
decoration: InputDecoration(
hintText: '搜索书名或作者…',
hintStyle: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.3)),
prefixIcon: Padding(
padding: const EdgeInsets.only(left: 14, right: 10),
child: Icon(Icons.search_rounded, size: 20, color: colors.onSurface.withValues(alpha: 0.35)),
),
prefixIconConstraints: const BoxConstraints(minWidth: 44, minHeight: 44),
suffixIcon: _query.isNotEmpty
? Padding(
padding: const EdgeInsets.only(right: 8),
child: GestureDetector(
onTap: () {
_searchCtrl.clear();
_onSearch('');
},
child: Container(
width: 24,
height: 24,
decoration: BoxDecoration(
color: colors.onSurface.withValues(alpha: 0.08),
shape: BoxShape.circle,
),
child: Icon(Icons.close_rounded, size: 14, color: colors.onSurface.withValues(alpha: 0.4)),
),
),
)
: null,
suffixIconConstraints: const BoxConstraints(minWidth: 32, minHeight: 32),
filled: true,
fillColor: colors.surfaceContainerHigh,
contentPadding: const EdgeInsets.symmetric(vertical: 12),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: colors.outlineVariant.withValues(alpha: 0.3), width: 1),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: colors.primary.withValues(alpha: 0.5), width: 1.5),
),
),
),
),
);
}
// ── 空状态 ──
Widget _buildEmptyState(ColorScheme colors) {
return Center(
child: Padding(
padding: const EdgeInsets.all(40),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 72,
height: 72,
decoration: BoxDecoration(
color: colors.surfaceContainerHighest,
borderRadius: BorderRadius.circular(20),
),
child: Icon(
_query.isEmpty ? Icons.library_books_outlined : Icons.search_off_rounded,
size: 36,
color: colors.onSurface.withValues(alpha: 0.2),
),
),
const SizedBox(height: 16),
Text(
_query.isEmpty ? '暂无书籍' : '未找到匹配的书籍',
style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.35)),
),
],
),
),
);
}
// ── 书籍卡片 ──
Widget _buildBookCard(Book book, ColorScheme colors, int index) {
final hasCover = book.coverPath != null && book.coverPath!.isNotEmpty;
return InkWell(
onTap: () => _selectBook(book),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
child: Row(children: [
// 封面缩略图
Container(
width: 44, height: 62,
decoration: BoxDecoration(
color: colors.surfaceContainerHighest,
borderRadius: BorderRadius.circular(6),
),
clipBehavior: Clip.antiAlias,
child: hasCover
? Image.asset(book.coverPath!, fit: BoxFit.cover,
errorBuilder: (_, __, ___) =>
Icon(Icons.book_outlined, size: 20, color: colors.onSurface.withValues(alpha: 0.2)))
: Icon(Icons.book_outlined, size: 20, color: colors.onSurface.withValues(alpha: 0.2)),
),
const SizedBox(width: 14),
// 书名 + 作者
Expanded(
child: Column(
return Padding(
padding: EdgeInsets.only(bottom: index < _filteredBooks.length - 1 ? 10 : 0),
child: Material(
color: colors.surfaceContainerHigh,
borderRadius: BorderRadius.circular(12),
clipBehavior: Clip.antiAlias,
child: InkWell(
onTap: () => _selectBook(book),
child: Container(
padding: const EdgeInsets.all(12),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(book.title, maxLines: 2, overflow: TextOverflow.ellipsis,
style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: colors.onSurface)),
if (book.authors.isNotEmpty) ...[
const SizedBox(height: 3),
Text(book.authors.join(', '), maxLines: 1, overflow: TextOverflow.ellipsis,
style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.35))),
],
// 封面
Container(
width: 48,
height: 68,
decoration: BoxDecoration(
color: colors.surfaceContainerHighest,
borderRadius: BorderRadius.circular(6),
boxShadow: [
BoxShadow(
color: colors.shadow.withValues(alpha: 0.1),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
clipBehavior: Clip.antiAlias,
child: hasCover
? FadeInLocalImage(
path: book.coverPath,
fit: BoxFit.cover,
)
: Center(
child: Icon(Icons.menu_book_outlined, size: 20, color: colors.onSurface.withValues(alpha: 0.2)),
),
),
const SizedBox(width: 14),
// 书名 + 作者
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 2),
Text(
book.title,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: colors.onSurface, height: 1.3),
),
if (book.authors.isNotEmpty) ...[
const SizedBox(height: 4),
Text(
book.authors.join(', '),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4)),
),
],
if (book.genres.isNotEmpty) ...[
const SizedBox(height: 6),
Wrap(
spacing: 4,
children: book.genres.take(2).map((g) => Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: colors.primary.withValues(alpha: 0.08),
borderRadius: BorderRadius.circular(4),
),
child: Text(
g,
style: TextStyle(fontSize: 10, color: colors.primary.withValues(alpha: 0.6)),
),
)).toList(),
),
],
],
),
),
const SizedBox(width: 8),
// 关联图标
Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(
color: colors.primary.withValues(alpha: 0.08),
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.link_rounded, size: 14, color: colors.primary),
const SizedBox(width: 4),
Text('关联', style: TextStyle(fontSize: 12, fontWeight: FontWeight.w500, color: colors.primary)),
],
),
),
],
),
),
Icon(Icons.chevron_right, size: 18, color: colors.onSurface.withValues(alpha: 0.25)),
]),
),
),
);
}