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:provider/provider.dart'; import '../../providers/app_provider.dart'; import '../../data/epub/reader_dao.dart'; import '../../widgets/genre_selector_page.dart'; import '../../widgets/text_input_panel.dart'; import '../../utils/image_path_helper.dart'; /// EPUB 书籍编辑页 class EpubEditPage extends StatefulWidget { final String bookId; final Map book; const EpubEditPage({ super.key, required this.bookId, required this.book, }); @override State createState() => _EpubEditPageState(); } class _EpubEditPageState extends State { final ReaderDao _dao = ReaderDao(); late TextEditingController _titleCtrl; late TextEditingController _summaryCtrl; late TextEditingController _publisherCtrl; late TextEditingController _isbnCtrl; List _authors = []; @override void initState() { super.initState(); _titleCtrl = TextEditingController(text: widget.book['title'] 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? ?? ''); // 解析多作者:优先用 authors(JSON 数组),否则从 author(逗号分隔)解析 final authorsJson = widget.book['authors'] as String? ?? ''; if (authorsJson.isNotEmpty) { try { _authors = List.from(jsonDecode(authorsJson)); } catch (_) { _authors = _parseAuthorField(authorsJson); } } else { final authorStr = widget.book['author'] as String? ?? ''; _authors = _parseAuthorField(authorStr); } } List _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(); _summaryCtrl.dispose(); _publisherCtrl.dispose(); _isbnCtrl.dispose(); super.dispose(); } Future _save() async { final newTitle = _titleCtrl.text.trim(); if (newTitle.isEmpty) return; await _dao.updateReaderBook(widget.bookId, { 'title': newTitle, '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); } Future _pickCover() async { final picker = ImagePicker(); final picked = await picker.pickImage(source: ImageSource.gallery, imageQuality: 85); if (picked == null || !mounted) return; final appDirPath = await ImagePathHelper.getAppDir(); final bookDir = Directory(p.join(appDirPath, 'epub_books', widget.bookId)); if (!await bookDir.exists()) await bookDir.create(recursive: true); final existing = bookDir.listSync().whereType().where((f) { final name = p.basenameWithoutExtension(f.path); return name.startsWith('cover_') && RegExp(r'^cover_\d+$').hasMatch(name); }).toList(); int nextIndex = 1; if (existing.isNotEmpty) { final indices = existing.map((f) { return int.tryParse(p.basenameWithoutExtension(f.path).substring(6)) ?? 0; }).toList(); indices.sort(); nextIndex = indices.last + 1; } final ext = p.extension(picked.path).toLowerCase(); final destPath = p.join(bookDir.path, 'cover_$nextIndex$ext'); await File(picked.path).copy(destPath); await _dao.updateReaderBook(widget.bookId, { 'cover_path': destPath, 'updated_at': DateTime.now().toIso8601String(), }); if (mounted) Navigator.pop(context, true); } Future _revertCover() async { final appDirPath = await ImagePathHelper.getAppDir(); final bookDir = Directory(p.join(appDirPath, 'epub_books', widget.bookId)); final existing = bookDir.listSync().whereType().where((f) { final name = p.basenameWithoutExtension(f.path); return name.startsWith('cover_') && RegExp(r'^cover_\d+$').hasMatch(name); }).toList(); if (existing.isEmpty) return; existing.sort((a, b) { final ia = int.tryParse(p.basenameWithoutExtension(a.path).substring(6)) ?? 0; final ib = int.tryParse(p.basenameWithoutExtension(b.path).substring(6)) ?? 0; return ia.compareTo(ib); }); final currentMax = existing.last; final currentIndex = int.tryParse(p.basenameWithoutExtension(currentMax.path).substring(6)) ?? 0; await currentMax.delete(); if (currentIndex <= 1) { final coverFile = bookDir.listSync().whereType().where((f) { final name = p.basenameWithoutExtension(f.path); return name == 'cover'; }).firstOrNull; await _dao.updateReaderBook(widget.bookId, { 'cover_path': coverFile?.path ?? '', 'updated_at': DateTime.now().toIso8601String(), }); } else { final prev = existing.where((f) { final idx = int.tryParse(p.basenameWithoutExtension(f.path).substring(6)) ?? 0; return idx == currentIndex - 1; }).firstOrNull; await _dao.updateReaderBook(widget.bookId, { 'cover_path': prev?.path ?? '', 'updated_at': DateTime.now().toIso8601String(), }); } 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, appBar: AppBar( backgroundColor: colors.surface, elevation: 0, title: Text('编辑书籍信息', style: TextStyle(fontSize: 17, fontWeight: FontWeight.w600, color: colors.onSurface)), leading: IconButton( icon: Icon(Icons.close, size: 20, color: colors.onSurface.withValues(alpha: 0.6)), onPressed: () => Navigator.pop(context), ), actions: [ TextButton( onPressed: _save, child: Text('保存', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: colors.primary)), ), const SizedBox(width: 4), ], ), body: ListView( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), children: [ // 封面 Center(child: _buildCoverPicker(coverPath, colors)), const SizedBox(height: 24), // 信息卡片 Wrap( spacing: 12, runSpacing: 12, children: [ // 标题 SizedBox( width: halfWidth, height: 90, child: _buildInfoCard( label: '标题', value: _titleCtrl.text, required: true, icon: Icons.auto_stories_outlined, onTap: () async { final result = await TextInputPanel.show( context: context, title: '书名', initialValue: _titleCtrl.text, hint: '请输入书名', ); if (result != null) setState(() => _titleCtrl.text = result); }, colors: colors, ), ), // 作者(多选) SizedBox( width: halfWidth, height: 90, child: _buildInfoCard( label: '作者', value: _authors.isEmpty ? '' : '${_authors.length}人:${_authors.join('、')}', icon: Icons.person_outline, onTap: () async { final provider = context.read(); 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: _publisherCtrl.text, hint: '请输入出版社', ); 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( context, MaterialPageRoute( builder: (_) => _SummaryEditorPage(initialText: _summaryCtrl.text), ), ); if (result != null) setState(() => _summaryCtrl.text = result); }, colors: colors, ), ), ], ), const SizedBox(height: 48), ], ), ); } // ─── 构建组件 ──────────────────────────────────────────────── Widget _buildCoverPicker(String? coverPath, ColorScheme colors) { 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)), ], ), ), 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, 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, borderRadius: BorderRadius.circular(10), border: Border.all(color: colors.outlineVariant, width: 0.5), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row(children: [ Icon(icon, size: 14, color: colors.onSurface.withValues(alpha: 0.4)), const SizedBox(width: 6), 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(), buildContent(), ], ), ), ); } } /// 简介 编辑页 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 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: 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 _collectUnique(List> lists) { final s = {}; for (final l in lists) { s.addAll(l); } return s.toList()..sort(); }