generated from dellevin/template
新增epub书摘,高亮文字,适配平板
This commit is contained in:
@@ -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)),
|
||||
]),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user