基本功能完善

This commit is contained in:
DelLevin-Home
2026-03-07 00:40:01 +08:00
parent f2ab6a17ba
commit 082145ff79
39 changed files with 5726 additions and 867 deletions

View File

@@ -1,8 +1,12 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:share_plus/share_plus.dart';
import 'package:cross_file/cross_file.dart';
import '../providers/app_provider.dart';
import '../models/data_models.dart';
import 'book_reviews_page.dart';
import 'book_excerpts_page.dart';
/// 书籍详情页 - 极简主义设计
class BookDetailPage extends StatefulWidget {
@@ -15,46 +19,65 @@ class BookDetailPage extends StatefulWidget {
}
class _BookDetailPageState extends State<BookDetailPage> {
@override
void didChangeDependencies() {
super.didChangeDependencies();
// 页面获得焦点时刷新数据
_refreshBookData();
}
void _refreshBookData() {
final provider = context.read<AppProvider>();
// 强制刷新当前书籍数据
provider.loadBooks();
}
@override
Widget build(BuildContext context) {
// 从 Provider 获取最新的 book 数据,实现动态刷新
final book = context.watch<AppProvider>().books
.where((b) => b.id == widget.book.id)
.firstOrNull ?? widget.book;
return Scaffold(
backgroundColor: Colors.white,
body: CustomScrollView(
slivers: [
// 顶部封面区域
_buildSliverAppBar(),
_buildSliverAppBar(book),
// 内容区域
SliverToBoxAdapter(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 基本信息
_buildBasicInfo(),
_buildBasicInfo(book),
const Divider(height: 0.5, thickness: 0.5, color: Color(0xFFE5E5E5)),
// 作者信息
_buildAuthorsSection(),
_buildAuthorsSection(book),
// 出版社
if (widget.book.publisher != null && widget.book.publisher!.isNotEmpty)
_buildPublisherSection(),
if (book.publisher != null && book.publisher!.isNotEmpty)
_buildPublisherSection(book),
// 类型
if (widget.book.genres.isNotEmpty)
_buildGenresSection(),
if (book.genres.isNotEmpty)
_buildGenresSection(book),
const Divider(height: 0.5, thickness: 0.5, color: Color(0xFFE5E5E5)),
// 简介
if (widget.book.summary != null && widget.book.summary!.isNotEmpty)
_buildSummarySection(),
// 别名
if (widget.book.alternateTitles.isNotEmpty)
_buildAlternateTitlesSection(),
if (book.summary != null && book.summary!.isNotEmpty)
_buildSummarySection(book),
const Divider(height: 0.5, thickness: 0.5, color: Color(0xFFE5E5E5)),
// 书评和摘抄入口
_buildExtraSections(book),
const SizedBox(height: 48),
],
),
@@ -68,33 +91,66 @@ class _BookDetailPageState extends State<BookDetailPage> {
}
/// 构建顶部 AppBar
Widget _buildSliverAppBar() {
Widget _buildSliverAppBar(Book book) {
final hasCover = book.coverPath != null && book.coverPath!.isNotEmpty;
return SliverAppBar(
expandedHeight: 280,
expandedHeight: 320,
pinned: true,
backgroundColor: Colors.white,
backgroundColor: const Color(0xFFF5F5F5),
flexibleSpace: FlexibleSpaceBar(
background: _buildCoverSection(),
background: _buildCoverSection(book),
),
actions: [
IconButton(
icon: const Icon(Icons.edit_outlined),
onPressed: () => _navigateToEdit(context),
// 下载封面按钮(仅当有封面时显示)
if (hasCover)
Container(
margin: const EdgeInsets.all(8),
decoration: const BoxDecoration(
color: Colors.white,
),
child: IconButton(
icon: const Icon(Icons.download_outlined, color: Color(0xFF666666)),
onPressed: () => _downloadCover(book),
tooltip: '下载封面',
),
),
// 清空封面按钮(仅当有封面时显示)
if (hasCover)
Container(
margin: const EdgeInsets.all(8),
decoration: const BoxDecoration(
color: Colors.white,
),
child: IconButton(
icon: const Icon(Icons.hide_image_outlined, color: Color(0xFF666666)),
onPressed: () => _showClearCoverDialog(book),
tooltip: '清空封面',
),
),
// 编辑按钮
Container(
margin: const EdgeInsets.all(8),
decoration: const BoxDecoration(
color: Colors.white,
),
child: IconButton(
icon: const Icon(Icons.edit_outlined, color: Color(0xFF1A1A1A)),
onPressed: () => _navigateToEdit(context),
),
),
const SizedBox(width: 8),
],
);
}
/// 构建封面区域
Widget _buildCoverSection() {
return Container(
width: double.infinity,
color: const Color(0xFFF5F5F5),
child: widget.book.coverPath != null && widget.book.coverPath!.isNotEmpty
Widget _buildCoverSection(Book book) {
return SizedBox.expand(
child: book.coverPath != null && book.coverPath!.isNotEmpty
? Image.file(
File(widget.book.coverPath!),
fit: BoxFit.contain,
File(book.coverPath!),
fit: BoxFit.cover,
errorBuilder: (_, __, ___) => _buildCoverPlaceholder(),
)
: _buildCoverPlaceholder(),
@@ -125,7 +181,7 @@ class _BookDetailPageState extends State<BookDetailPage> {
}
/// 构建基本信息
Widget _buildBasicInfo() {
Widget _buildBasicInfo(Book book) {
return Padding(
padding: const EdgeInsets.all(24),
child: Column(
@@ -133,7 +189,7 @@ class _BookDetailPageState extends State<BookDetailPage> {
children: [
// 书名
Text(
widget.book.title,
book.title,
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.w600,
@@ -141,13 +197,26 @@ class _BookDetailPageState extends State<BookDetailPage> {
height: 1.3,
),
),
// 别名(显示在主名称下面,用 / 分隔)
if (book.alternateTitles.isNotEmpty) ...[
const SizedBox(height: 8),
Text(
book.alternateTitles.join(' / '),
style: const TextStyle(
fontSize: 14,
color: Color(0xFF999999),
height: 1.4,
),
),
],
const SizedBox(height: 16),
// 评分和状态
Row(
children: [
if (widget.book.rating != null) ...[
if (book.rating != null) ...[
const Icon(
Icons.star,
size: 20,
@@ -155,7 +224,7 @@ class _BookDetailPageState extends State<BookDetailPage> {
),
const SizedBox(width: 4),
Text(
widget.book.rating!.toStringAsFixed(1),
book.rating!.toStringAsFixed(1),
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
@@ -164,15 +233,15 @@ class _BookDetailPageState extends State<BookDetailPage> {
),
const SizedBox(width: 16),
],
_buildStatusTag(),
_buildStatusTag(book),
],
),
const SizedBox(height: 8),
// 时间信息
Text(
'添加于 ${_formatDate(widget.book.createdAt)}',
'添加于 ${_formatDate(book.createdAt)}',
style: const TextStyle(
fontSize: 12,
color: Color(0xFF999999),
@@ -182,12 +251,12 @@ class _BookDetailPageState extends State<BookDetailPage> {
),
);
}
/// 构建状态标签
Widget _buildStatusTag() {
Widget _buildStatusTag(Book book) {
String label;
Color color;
switch (widget.book.status) {
switch (book.status) {
case 'read':
label = '已读';
color = const Color(0xFF1A1A1A);
@@ -222,7 +291,7 @@ class _BookDetailPageState extends State<BookDetailPage> {
}
/// 构建作者区域
Widget _buildAuthorsSection() {
Widget _buildAuthorsSection(Book book) {
return Padding(
padding: const EdgeInsets.all(24),
child: Column(
@@ -241,7 +310,7 @@ class _BookDetailPageState extends State<BookDetailPage> {
Wrap(
spacing: 8,
runSpacing: 8,
children: widget.book.authors.map((author) {
children: book.authors.map((author) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
@@ -264,7 +333,7 @@ class _BookDetailPageState extends State<BookDetailPage> {
}
/// 构建出版社区域
Widget _buildPublisherSection() {
Widget _buildPublisherSection(Book book) {
return Padding(
padding: const EdgeInsets.fromLTRB(24, 0, 24, 24),
child: Column(
@@ -281,7 +350,7 @@ class _BookDetailPageState extends State<BookDetailPage> {
),
const SizedBox(height: 8),
Text(
widget.book.publisher!,
book.publisher!,
style: const TextStyle(
fontSize: 15,
color: Color(0xFF1A1A1A),
@@ -293,7 +362,7 @@ class _BookDetailPageState extends State<BookDetailPage> {
}
/// 构建类型区域
Widget _buildGenresSection() {
Widget _buildGenresSection(Book book) {
return Padding(
padding: const EdgeInsets.fromLTRB(24, 0, 24, 24),
child: Column(
@@ -312,7 +381,7 @@ class _BookDetailPageState extends State<BookDetailPage> {
Wrap(
spacing: 8,
runSpacing: 8,
children: widget.book.genres.map((genre) {
children: book.genres.map((genre) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
@@ -334,7 +403,7 @@ class _BookDetailPageState extends State<BookDetailPage> {
}
/// 构建简介区域
Widget _buildSummarySection() {
Widget _buildSummarySection(Book book) {
return Padding(
padding: const EdgeInsets.all(24),
child: Column(
@@ -351,7 +420,7 @@ class _BookDetailPageState extends State<BookDetailPage> {
),
const SizedBox(height: 12),
Text(
widget.book.summary!,
book.summary!,
style: const TextStyle(
fontSize: 15,
color: Color(0xFF1A1A1A),
@@ -362,16 +431,16 @@ class _BookDetailPageState extends State<BookDetailPage> {
),
);
}
/// 构建别名区域
Widget _buildAlternateTitlesSection() {
/// 构建额外功能区域(书评、摘抄)
Widget _buildExtraSections(Book book) {
return Padding(
padding: const EdgeInsets.fromLTRB(24, 0, 24, 24),
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'别名',
'更多',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
@@ -379,25 +448,137 @@ class _BookDetailPageState extends State<BookDetailPage> {
letterSpacing: 1,
),
),
const SizedBox(height: 8),
Wrap(
spacing: 8,
runSpacing: 8,
children: widget.book.alternateTitles.map((title) {
return Text(
title,
style: const TextStyle(
fontSize: 14,
color: Color(0xFF666666),
),
);
}).toList(),
const SizedBox(height: 16),
// 书评入口
GestureDetector(
onTap: () => _navigateToReviews(book),
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
border: Border.all(color: const Color(0xFFE5E5E5)),
),
child: Row(
children: [
const Icon(
Icons.rate_review_outlined,
size: 24,
color: Color(0xFF666666),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'书评',
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w500,
color: Color(0xFF1A1A1A),
),
),
const SizedBox(height: 4),
FutureBuilder<int>(
future: context.read<AppProvider>().getBookReviewCount(book.id),
builder: (context, snapshot) {
final count = snapshot.data ?? 0;
return Text(
count > 0 ? '$count 条书评' : '暂无书评',
style: const TextStyle(
fontSize: 13,
color: Color(0xFF999999),
),
);
},
),
],
),
),
const Icon(
Icons.chevron_right,
color: Color(0xFF999999),
),
],
),
),
),
const SizedBox(height: 12),
// 摘抄入口
GestureDetector(
onTap: () => _navigateToExcerpts(book),
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
border: Border.all(color: const Color(0xFFE5E5E5)),
),
child: Row(
children: [
const Icon(
Icons.format_quote_outlined,
size: 24,
color: Color(0xFF666666),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'摘抄',
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w500,
color: Color(0xFF1A1A1A),
),
),
const SizedBox(height: 4),
FutureBuilder<int>(
future: context.read<AppProvider>().getBookExcerptCount(book.id),
builder: (context, snapshot) {
final count = snapshot.data ?? 0;
return Text(
count > 0 ? '$count 条摘抄' : '暂无摘抄',
style: const TextStyle(
fontSize: 13,
color: Color(0xFF999999),
),
);
},
),
],
),
),
const Icon(
Icons.chevron_right,
color: Color(0xFF999999),
),
],
),
),
),
],
),
);
}
void _navigateToReviews(Book book) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => BookReviewsPage(book: book),
),
);
}
void _navigateToExcerpts(Book book) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => BookExcerptsPage(book: book),
),
);
}
/// 构建底部操作栏
Widget _buildBottomBar() {
return Container(
@@ -486,4 +667,80 @@ class _BookDetailPageState extends State<BookDetailPage> {
),
);
}
/// 显示清空封面对话框
void _showClearCoverDialog(Book book) {
showDialog(
context: context,
builder: (context) => AlertDialog(
backgroundColor: Colors.white,
elevation: 0,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
title: const Text('清空封面'),
content: const Text('确定要清空封面吗?清空后将使用默认占位图。'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('取消', style: TextStyle(color: Color(0xFF666666))),
),
TextButton(
onPressed: () async {
Navigator.pop(context);
final updatedBook = book.copyWith(
coverPath: null,
updatedAt: DateTime.now(),
);
await context.read<AppProvider>().updateBook(updatedBook);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('封面已清空')),
);
}
},
child: const Text('清空', style: TextStyle(color: Colors.red)),
),
],
),
);
}
/// 下载封面到本地
Future<void> _downloadCover(Book book) async {
if (book.coverPath == null || book.coverPath!.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('没有可下载的封面')),
);
return;
}
try {
final sourceFile = File(book.coverPath!);
if (!await sourceFile.exists()) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('封面文件不存在')),
);
return;
}
// 生成文件名书籍名称_时间戳_封面.jpg
final timestamp = DateTime.now().millisecondsSinceEpoch;
final fileName = '${book.title}_${timestamp}_封面.jpg';
// 获取临时目录路径
final tempDir = await Directory.systemTemp.createTemp();
final tempFile = File('${tempDir.path}/$fileName');
await sourceFile.copy(tempFile.path);
// 使用分享功能让用户选择保存位置
await Share.shareXFiles(
[XFile(tempFile.path)],
subject: '${book.title} 封面',
text: '下载自 MookNote',
);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('下载失败: $e')),
);
}
}
}