readful/lib/components/app_header.dart
ddshi bef0de5909 feat: 完成文件导入功能和Book模型扩展
## 新增功能
- EPUB文件导入功能:支持文件选择、解析和存储
- 文件重复检测:避免重复导入同一文件
- 导入状态反馈:成功/失败消息提示

## 模型扩展
- Book模型新增多作者支持(authors字段)
- 新增章节数统计(chapterCount字段)
- 新增语言标识(language字段)
- 新增EPUB标识符(identifier字段)
- 优化TypeAdapter序列化支持

## 服务优化
- 新增EpubParserService:EPUB文件解析服务
- 改进DatabaseService:错误处理和数据迁移
- 优化BookRepository:调试日志和错误追踪

## 依赖更新
- 新增epubx ^4.0.0:EPUB电子书解析库
- 更新pubspec.lock:同步依赖版本

## UI改进
- AppHeader组件集成完整导入功能
- SafeArea适配:避免系统状态栏重叠
- 优化测试数据结构

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-04 20:34:16 +08:00

234 lines
6.8 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'package:flutter/material.dart';
import '../pages/search_page.dart';
import '../services/epub_parser_service.dart';
import '../services/book_repository.dart';
import 'package:file_picker/file_picker.dart';
/// 应用顶部导航组件
///
/// 可复用的顶部导航栏组件,集成在搜索栏内的导入按钮。
/// 提供统一的搜索和文件导入功能,支持主题自适应。
class AppHeader extends StatefulWidget {
/// 标题文本(可选),某些页面可以显示标题而非搜索栏
final String? title;
/// 搜索按钮点击回调函数
final VoidCallback? onSearchPressed;
/// 导入按钮点击回调函数
final VoidCallback? onImportPressed;
/// 是否显示搜索栏默认值为true
final bool showSearchBar;
/// 搜索栏占位符文本
final String searchHint;
const AppHeader({
super.key,
this.title,
this.onSearchPressed,
this.onImportPressed,
this.showSearchBar = true,
this.searchHint = '搜索书名或内容...',
});
@override
State<AppHeader> createState() => _AppHeaderState();
}
class _AppHeaderState extends State<AppHeader> {
final EpubParserService _epubParserService = EpubParserService();
final BookRepository _bookRepository = BookRepository();
/// 构建组件的UI结构
@override
Widget build(BuildContext context) {
return Container(
height: 60,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
border: Border(
bottom: BorderSide(
color: Theme.of(context).colorScheme.outline.withOpacity(0.2),
width: 1,
),
),
),
child: Row(
children: [
// 标题区域(条件渲染)
if (widget.title != null) ...[
Text(
widget.title!,
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(width: 16),
],
// 搜索栏区域(占据剩余空间)
if (widget.showSearchBar) ...[
Expanded(
child: _buildSearchBar(context),
),
]
],
),
);
}
/// 构建搜索栏组件,包含搜索图标、占位符文本和导入按钮
Widget _buildSearchBar(BuildContext context) {
return Container(
height: 44,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceVariant,
borderRadius: BorderRadius.circular(22),
border: Border.all(
color: Theme.of(context).colorScheme.outline.withOpacity(0.3),
),
),
child: Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(22),
onTap: widget.onSearchPressed ?? () => _navigateToSearchPage(context),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
Icon(
Icons.search,
size: 24,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
const SizedBox(width: 8),
Expanded(
child: Text(
widget.searchHint,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context)
.colorScheme
.onSurfaceVariant
.withOpacity(0.6),
),
),
),
const SizedBox(width: 8),
_buildImportButton(context),
],
),
),
),
),
);
}
/// 构建导入按钮组件
Widget _buildImportButton(BuildContext context) {
return Container(
height: 32,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(16),
),
child: Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(16),
onTap: widget.onImportPressed ?? () => _importEpubFile(),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'导入',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onPrimaryContainer,
),
),
],
),
),
),
),
);
}
/// 导航到搜索页面
void _navigateToSearchPage(BuildContext context) {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const SearchPage(),
),
);
}
/// 导入EPUB文件
Future<void> _importEpubFile() async {
try {
// 1. 使用FilePicker选择EPUB文件
FilePickerResult? result = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: ['epub'],
);
if (result != null && result.files.single.path != null) {
final filePath = result.files.single.path!;
// 2. 检查文件是否已存在
final existingBooks = await _bookRepository.getAllBooks();
final isDuplicate = existingBooks.any((book) => book.filePath == filePath);
if (isDuplicate) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('该文件已经导入过了'),
backgroundColor: Colors.orange,
),
);
}
return;
}
// 3. 解析EPUB文件
final epubBook = await _epubParserService.parseEpubFile(filePath);
// 4. 提取元数据并创建Book对象
final book = await _epubParserService.extractBookMetadata(epubBook, filePath);
// 5. 保存到数据库
await _bookRepository.addBook(book);
// 6. 显示成功消息
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('成功导入: ${book.title}'),
backgroundColor: Colors.green,
duration: const Duration(seconds: 2),
),
);
}
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('导入失败: $e'),
backgroundColor: Colors.red,
duration: const Duration(seconds: 3),
),
);
}
}
}
}