优化epub功能和清除缓存功能

This commit is contained in:
DelLevin-Home
2026-07-05 03:13:06 +08:00
parent 1529c7d7cb
commit 82aca12402
10 changed files with 714 additions and 142 deletions

View File

@@ -1,11 +1,16 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import 'package:provider/provider.dart';
import '../../providers/app_provider.dart';
import '../../utils/epub/reader_dao.dart';
import '../../widgets/genre_selector_page.dart';
import '../../widgets/text_input_panel.dart';
/// EPUB 书籍编辑页
@@ -27,19 +32,44 @@ class _EpubEditPageState extends State<EpubEditPage> {
final ReaderDao _dao = ReaderDao();
late TextEditingController _titleCtrl;
late TextEditingController _authorCtrl;
late TextEditingController _summaryCtrl;
late TextEditingController _publisherCtrl;
late TextEditingController _isbnCtrl;
List<String> _authors = [];
@override
void initState() {
super.initState();
_titleCtrl = TextEditingController(text: widget.book['title'] as String? ?? '');
_authorCtrl = TextEditingController(text: widget.book['author'] as String? ?? '');
_summaryCtrl = TextEditingController(text: widget.book['summary'] as String? ?? '');
_publisherCtrl = TextEditingController(text: widget.book['publisher'] as String? ?? '');
_isbnCtrl = TextEditingController(text: widget.book['isbn'] as String? ?? '');
// 解析多作者:优先用 authorsJSON 数组),否则从 author逗号分隔解析
final authorsJson = widget.book['authors'] as String? ?? '';
if (authorsJson.isNotEmpty) {
try {
_authors = List<String>.from(jsonDecode(authorsJson));
} catch (_) {
_authors = _parseAuthorField(authorsJson);
}
} else {
final authorStr = widget.book['author'] as String? ?? '';
_authors = _parseAuthorField(authorStr);
}
}
List<String> _parseAuthorField(String text) {
if (text.isEmpty) return [];
return text.split(RegExp(r'[,、/]')).map((s) => s.trim()).where((s) => s.isNotEmpty).toList();
}
@override
void dispose() {
_titleCtrl.dispose();
_authorCtrl.dispose();
_summaryCtrl.dispose();
_publisherCtrl.dispose();
_isbnCtrl.dispose();
super.dispose();
}
@@ -48,7 +78,11 @@ class _EpubEditPageState extends State<EpubEditPage> {
if (newTitle.isEmpty) return;
await _dao.updateReaderBook(widget.bookId, {
'title': newTitle,
'author': _authorCtrl.text.trim(),
'author': _authors.join(''),
'authors': jsonEncode(_authors),
'summary': _summaryCtrl.text.trim(),
'publisher': _publisherCtrl.text.trim(),
'isbn': _isbnCtrl.text.trim(),
'updated_at': DateTime.now().toIso8601String(),
});
if (mounted) Navigator.pop(context, true);
@@ -129,10 +163,13 @@ class _EpubEditPageState extends State<EpubEditPage> {
if (mounted) Navigator.pop(context, true);
}
bool get _hasLinkedBook => (widget.book['book_id'] as String? ?? '').isNotEmpty;
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
final coverPath = widget.book['cover_path'] as String?;
final halfWidth = (MediaQuery.of(context).size.width - 52) / 2;
return Scaffold(
backgroundColor: colors.surface,
@@ -156,7 +193,7 @@ class _EpubEditPageState extends State<EpubEditPage> {
body: ListView(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
children: [
// 封面选择
// 封面
Center(child: _buildCoverPicker(coverPath, colors)),
const SizedBox(height: 24),
// 信息卡片
@@ -164,8 +201,9 @@ class _EpubEditPageState extends State<EpubEditPage> {
spacing: 12,
runSpacing: 12,
children: [
// 标题
SizedBox(
width: (MediaQuery.of(context).size.width - 52) / 2,
width: halfWidth,
height: 90,
child: _buildInfoCard(
label: '标题',
@@ -184,46 +222,94 @@ class _EpubEditPageState extends State<EpubEditPage> {
colors: colors,
),
),
// 作者(多选)
SizedBox(
width: (MediaQuery.of(context).size.width - 52) / 2,
width: halfWidth,
height: 90,
child: _buildInfoCard(
label: '作者',
value: _authorCtrl.text,
value: _authors.isEmpty ? '' : '${_authors.length}人:${_authors.join('')}',
icon: Icons.person_outline,
onTap: () async {
final provider = context.read<AppProvider>();
final data = provider.books.map((b) => b.authors).toList();
final result = await GenreSelectorPage.show(
context: context,
title: '选择作者',
existingTagsFuture: compute(_collectUnique, data),
initialSelected: _authors,
hint: '如:余华、莫言',
);
if (result != null) setState(() => _authors = result);
},
colors: colors,
),
),
// 出版社
SizedBox(
width: halfWidth,
height: 90,
child: _buildInfoCard(
label: '出版社',
value: _publisherCtrl.text,
icon: Icons.business_outlined,
onTap: () async {
final result = await TextInputPanel.show(
context: context,
title: '作者',
initialValue: _authorCtrl.text,
hint: '请输入作者',
title: '出版社',
initialValue: _publisherCtrl.text,
hint: '请输入出版社',
);
if (result != null) setState(() => _authorCtrl.text = result);
if (result != null) setState(() => _publisherCtrl.text = result);
},
colors: colors,
),
),
// ISBN
SizedBox(
width: halfWidth,
height: 90,
child: _buildInfoCard(
label: 'ISBN',
value: _isbnCtrl.text,
icon: Icons.qr_code_outlined,
onTap: () async {
final result = await TextInputPanel.show(
context: context,
title: 'ISBN',
initialValue: _isbnCtrl.text,
hint: '请输入ISBN编号',
keyboardType: TextInputType.number,
);
if (result != null) setState(() => _isbnCtrl.text = result);
},
colors: colors,
),
),
// 简介(全宽)
SizedBox(
width: double.infinity,
child: _buildInfoCard(
label: '简介',
value: _summaryCtrl.text,
icon: Icons.description_outlined,
height: 160,
scrollable: true,
onTap: () async {
final result = await Navigator.push<String>(
context,
MaterialPageRoute(
builder: (_) => _SummaryEditorPage(initialText: _summaryCtrl.text),
),
);
if (result != null) setState(() => _summaryCtrl.text = result);
},
colors: colors,
),
),
],
),
const SizedBox(height: 24),
// 封面操作
Text('封面操作',
style: TextStyle(fontSize: 11, fontWeight: FontWeight.w600,
color: colors.onSurface.withValues(alpha: 0.4))),
const SizedBox(height: 10),
Row(children: [
Expanded(child: _buildActionCard(
icon: Icons.add_photo_alternate_outlined, title: '更换封面', subtitle: '从相册选择',
color: colors.primary,
onTap: _pickCover,
)),
const SizedBox(width: 10),
Expanded(child: _buildActionCard(
icon: Icons.undo, title: '恢复上次', subtitle: '回退到上一个封面',
color: colors.onSurface.withValues(alpha: 0.5),
onTap: _revertCover,
)),
]),
const SizedBox(height: 48),
],
),
);
@@ -232,40 +318,130 @@ class _EpubEditPageState extends State<EpubEditPage> {
// ─── 构建组件 ────────────────────────────────────────────────
Widget _buildCoverPicker(String? coverPath, ColorScheme colors) {
return GestureDetector(
onTap: _pickCover,
child: Container(
width: 110, height: 154,
decoration: BoxDecoration(
color: colors.surfaceContainerHighest,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: colors.outlineVariant, width: 0.5),
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 110,
height: 154,
decoration: BoxDecoration(
color: colors.surfaceContainerHighest,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: colors.outlineVariant, width: 0.5),
),
clipBehavior: Clip.antiAlias,
child: coverPath != null && coverPath.isNotEmpty && File(coverPath).existsSync()
? Image.file(File(coverPath), fit: BoxFit.cover)
: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.auto_stories_outlined, size: 36,
color: colors.onSurface.withValues(alpha: 0.2)),
],
),
),
clipBehavior: Clip.antiAlias,
child: coverPath != null && coverPath.isNotEmpty && File(coverPath).existsSync()
? Image.file(File(coverPath), fit: BoxFit.cover)
: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.add_photo_alternate_outlined, size: 28,
color: colors.onSurface.withValues(alpha: 0.25)),
const SizedBox(height: 6),
Text('点击更换',
style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.3))),
],
if (!_hasLinkedBook) ...[
const SizedBox(height: 10),
Row(
mainAxisSize: MainAxisSize.min,
children: [
GestureDetector(
onTap: _pickCover,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: colors.surfaceContainerHighest,
borderRadius: BorderRadius.circular(16),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.add_photo_alternate_outlined, size: 14,
color: colors.onSurface.withValues(alpha: 0.6)),
const SizedBox(width: 4),
Text('更换封面',
style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.6))),
],
),
),
),
),
const SizedBox(width: 8),
GestureDetector(
onTap: _revertCover,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: colors.surfaceContainerHighest,
borderRadius: BorderRadius.circular(16),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.undo, size: 14,
color: colors.onSurface.withValues(alpha: 0.6)),
const SizedBox(width: 4),
Text('恢复上次',
style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.6))),
],
),
),
),
],
),
],
if (_hasLinkedBook) ...[
const SizedBox(height: 8),
Text('封面由关联书籍提供',
style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.35))),
],
],
);
}
Widget _buildInfoCard({
required String label, required String value, required IconData icon,
required VoidCallback onTap, required ColorScheme colors,
required String label,
required String value,
required IconData icon,
required VoidCallback onTap,
required ColorScheme colors,
bool required = false,
double? height,
bool scrollable = false,
}) {
final hasValue = value.isNotEmpty;
Widget buildContent() {
if (scrollable && height != null) {
return Flexible(
child: SingleChildScrollView(
physics: const BouncingScrollPhysics(),
child: Text(
hasValue ? value : '点击填写',
style: TextStyle(
fontSize: 14,
color: hasValue ? colors.onSurface : colors.onSurface.withValues(alpha: 0.2),
fontWeight: hasValue ? FontWeight.w500 : FontWeight.normal,
),
),
),
);
}
return Text(
hasValue ? value : '未设置',
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 14,
fontWeight: hasValue ? FontWeight.w500 : FontWeight.normal,
color: hasValue ? colors.onSurface : colors.onSurface.withValues(alpha: 0.2),
),
);
}
return GestureDetector(
onTap: onTap,
child: Container(
height: height,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: colors.surfaceContainerHigh,
@@ -278,61 +454,86 @@ class _EpubEditPageState extends State<EpubEditPage> {
Row(children: [
Icon(icon, size: 14, color: colors.onSurface.withValues(alpha: 0.4)),
const SizedBox(width: 6),
Text(label, style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.4))),
Text(label,
style: TextStyle(
fontSize: 11,
color: required ? colors.onSurface : colors.onSurface.withValues(alpha: 0.4),
fontWeight: required ? FontWeight.w500 : FontWeight.normal,
)),
if (required)
Text(' *', style: TextStyle(fontSize: 11, color: colors.error)),
]),
const Spacer(),
Text(
value.isEmpty ? '未设置' : value,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 14, fontWeight: FontWeight.w500,
color: value.isEmpty ? colors.onSurface.withValues(alpha: 0.2) : colors.onSurface,
),
),
buildContent(),
],
),
),
);
}
}
Widget _buildActionCard({
required IconData icon, required String title, required String subtitle,
required Color color, required VoidCallback onTap,
}) {
/// 简介 编辑页
class _SummaryEditorPage extends StatefulWidget {
final String initialText;
const _SummaryEditorPage({required this.initialText});
@override
State<_SummaryEditorPage> createState() => _SummaryEditorPageState();
}
class _SummaryEditorPageState extends State<_SummaryEditorPage> {
late final TextEditingController _controller;
@override
void initState() {
super.initState();
_controller = TextEditingController(text: widget.initialText);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: colors.surfaceContainerHigh,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: colors.outlineVariant, width: 0.5),
),
child: Row(children: [
Container(
width: 36, height: 36,
decoration: BoxDecoration(
color: colors.surface,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: colors.outlineVariant, width: 0.5),
),
child: Icon(icon, size: 18, color: color),
return Scaffold(
backgroundColor: colors.surface,
appBar: AppBar(
title: const Text('简介'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, _controller.text.trim()),
child: Text('完成',
style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: colors.primary)),
),
const SizedBox(width: 10),
Expanded(child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: colors.onSurface)),
const SizedBox(height: 2),
Text(subtitle, style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.4))),
],
)),
]),
const SizedBox(width: 8),
],
),
body: TextField(
controller: _controller,
maxLines: null,
expands: true,
textAlignVertical: TextAlignVertical.top,
style: TextStyle(fontSize: 15, color: colors.onSurface, height: 1.6),
decoration: InputDecoration(
hintText: '写下书籍简介...',
hintStyle: TextStyle(color: colors.onSurface.withValues(alpha: 0.3)),
contentPadding: const EdgeInsets.all(20),
border: InputBorder.none,
),
),
);
}
}
/// 从多值字段列表中提取去重排序的唯一值(供 compute 使用)
List<String> _collectUnique(List<List<String>> lists) {
final s = <String>{};
for (final l in lists) {
s.addAll(l);
}
return s.toList()..sort();
}