generated from dellevin/template
292 lines
9.6 KiB
Dart
292 lines
9.6 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
|
|
import '../../utils/epub/reader_dao.dart';
|
|
import '../../utils/toast_util.dart';
|
|
import '../../utils/user_prefs.dart';
|
|
import 'highlight_detail_sheet.dart';
|
|
|
|
/// EPUB 句读(高亮)管理页面 —— 支持瀑布流/列表模式切换
|
|
class EpubHighlightsPage extends StatefulWidget {
|
|
final String bookId;
|
|
final Map<String, dynamic> book;
|
|
|
|
const EpubHighlightsPage({
|
|
super.key,
|
|
required this.bookId,
|
|
required this.book,
|
|
});
|
|
|
|
@override
|
|
State<EpubHighlightsPage> createState() => _EpubHighlightsPageState();
|
|
}
|
|
|
|
class _EpubHighlightsPageState extends State<EpubHighlightsPage> {
|
|
final ReaderDao _dao = ReaderDao();
|
|
List<Map<String, dynamic>> _highlights = [];
|
|
bool _isLoading = true;
|
|
bool _isListMode = UserPrefs().highlightsViewMode == 1;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_loadHighlights();
|
|
}
|
|
|
|
Future<void> _loadHighlights() async {
|
|
setState(() => _isLoading = true);
|
|
try {
|
|
final all = await _dao.getHighlightsByBookId(widget.bookId);
|
|
setState(() {
|
|
_highlights = all.where((h) => h['color'] != 'excerpt').toList();
|
|
_isLoading = false;
|
|
});
|
|
} catch (e) {
|
|
setState(() => _isLoading = false);
|
|
if (mounted) ToastUtil.show(context, '加载失败: $e');
|
|
}
|
|
}
|
|
|
|
void _toggleViewMode() {
|
|
setState(() => _isListMode = !_isListMode);
|
|
UserPrefs().setHighlightsViewMode(_isListMode ? 1 : 0);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final colors = Theme.of(context).colorScheme;
|
|
return Scaffold(
|
|
backgroundColor: colors.surface,
|
|
appBar: AppBar(
|
|
title: Text('句读', style: TextStyle(fontSize: 17, fontWeight: FontWeight.w600)),
|
|
actions: [
|
|
IconButton(
|
|
icon: Icon(
|
|
_isListMode ? Icons.grid_view_rounded : Icons.view_agenda_outlined,
|
|
size: 22,
|
|
),
|
|
tooltip: _isListMode ? '瀑布流' : '列表',
|
|
onPressed: _toggleViewMode,
|
|
),
|
|
],
|
|
),
|
|
body: _isLoading
|
|
? Center(child: CircularProgressIndicator(color: colors.primary))
|
|
: _highlights.isEmpty
|
|
? _buildEmpty(colors)
|
|
: _isListMode
|
|
? _buildListView(colors)
|
|
: _buildMasonryView(colors),
|
|
);
|
|
}
|
|
|
|
// ─── 瀑布流模式 ───
|
|
|
|
Widget _buildMasonryView(ColorScheme colors) {
|
|
return MasonryGridView.count(
|
|
padding: const EdgeInsets.fromLTRB(12, 8, 12, 24),
|
|
crossAxisCount: 2,
|
|
mainAxisSpacing: 8,
|
|
crossAxisSpacing: 8,
|
|
itemCount: _highlights.length,
|
|
itemBuilder: (context, i) => _buildMasonryCard(_highlights[i], colors),
|
|
);
|
|
}
|
|
|
|
Widget _buildMasonryCard(Map<String, dynamic> highlight, ColorScheme colors) {
|
|
final content = highlight['content'] as String? ?? '';
|
|
final chapter = highlight['chapter'] as String? ?? '';
|
|
final chapterNum = int.tryParse(chapter);
|
|
final createdAt = highlight['created_at'] as String? ?? '';
|
|
|
|
return GestureDetector(
|
|
onTap: () => _showDetail(highlight, colors),
|
|
onLongPress: () => _showDeleteConfirm(highlight['id'] as int, colors),
|
|
child: Container(
|
|
padding: const EdgeInsets.fromLTRB(12, 10, 12, 10),
|
|
decoration: BoxDecoration(
|
|
color: colors.surfaceContainerHigh,
|
|
borderRadius: BorderRadius.circular(10),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// 高亮文本
|
|
Text(
|
|
content,
|
|
style: TextStyle(
|
|
fontSize: 13,
|
|
color: colors.onSurface.withValues(alpha: 0.85),
|
|
height: 1.65,
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
// 底部信息行
|
|
Row(
|
|
children: [
|
|
if (chapterNum != null) ...[
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 1.5),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFFFFEB3B).withValues(alpha: 0.18),
|
|
borderRadius: BorderRadius.circular(4),
|
|
),
|
|
child: Text(
|
|
'第${chapterNum + 1}章',
|
|
style: const TextStyle(fontSize: 9, color: Color(0xFF795548), fontWeight: FontWeight.w500),
|
|
),
|
|
),
|
|
],
|
|
if (createdAt.isNotEmpty) ...[
|
|
const SizedBox(width: 6),
|
|
Text(
|
|
_formatDate(createdAt),
|
|
style: TextStyle(fontSize: 9, color: colors.onSurface.withValues(alpha: 0.3)),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// ─── 列表模式 ───
|
|
|
|
Widget _buildListView(ColorScheme colors) {
|
|
return ListView.separated(
|
|
padding: const EdgeInsets.fromLTRB(16, 8, 16, 24),
|
|
itemCount: _highlights.length,
|
|
separatorBuilder: (_, __) => const SizedBox(height: 8),
|
|
itemBuilder: (context, i) => _buildListCard(_highlights[i], colors),
|
|
);
|
|
}
|
|
|
|
Widget _buildListCard(Map<String, dynamic> highlight, ColorScheme colors) {
|
|
final content = highlight['content'] as String? ?? '';
|
|
final chapter = highlight['chapter'] as String? ?? '';
|
|
final chapterNum = int.tryParse(chapter);
|
|
final createdAt = highlight['created_at'] as String? ?? '';
|
|
|
|
return GestureDetector(
|
|
onTap: () => _showDetail(highlight, colors),
|
|
onLongPress: () => _showDeleteConfirm(highlight['id'] as int, colors),
|
|
child: Container(
|
|
padding: const EdgeInsets.fromLTRB(14, 14, 14, 12),
|
|
decoration: BoxDecoration(
|
|
color: colors.surfaceContainerHigh,
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// 章节 + 日期
|
|
Row(
|
|
children: [
|
|
Icon(Icons.highlight_outlined, size: 13, color: const Color(0xFFFFC107)),
|
|
if (chapterNum != null) ...[
|
|
const SizedBox(width: 5),
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFFFFEB3B).withValues(alpha: 0.18),
|
|
borderRadius: BorderRadius.circular(4),
|
|
),
|
|
child: Text(
|
|
'第${chapterNum + 1}章',
|
|
style: const TextStyle(fontSize: 10, color: Color(0xFF795548), fontWeight: FontWeight.w500),
|
|
),
|
|
),
|
|
],
|
|
const Spacer(),
|
|
if (createdAt.isNotEmpty)
|
|
Text(
|
|
_formatDateFull(createdAt),
|
|
style: TextStyle(fontSize: 10, color: colors.onSurface.withValues(alpha: 0.3)),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 10),
|
|
// 高亮文本
|
|
Text(
|
|
content,
|
|
maxLines: 4,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
color: colors.onSurface.withValues(alpha: 0.85),
|
|
height: 1.75,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// ─── 公共 ───
|
|
|
|
Widget _buildEmpty(ColorScheme colors) {
|
|
return Center(
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(Icons.highlight_outlined, size: 48, color: colors.onSurface.withValues(alpha: 0.2)),
|
|
const SizedBox(height: 12),
|
|
Text('暂无句读', style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.35))),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
void _showDetail(Map<String, dynamic> highlight, ColorScheme colors) {
|
|
showHighlightDetailSheet(
|
|
context,
|
|
highlight: highlight,
|
|
book: widget.book,
|
|
onDelete: () => _deleteHighlight(highlight['id'] as int),
|
|
);
|
|
}
|
|
|
|
String _formatDate(String isoString) {
|
|
try {
|
|
final dt = DateTime.parse(isoString);
|
|
return '${dt.month.toString().padLeft(2, '0')}-${dt.day.toString().padLeft(2, '0')}';
|
|
} catch (_) {
|
|
return '';
|
|
}
|
|
}
|
|
|
|
String _formatDateFull(String isoString) {
|
|
try {
|
|
final dt = DateTime.parse(isoString);
|
|
return '${dt.year}-${dt.month.toString().padLeft(2, '0')}-${dt.day.toString().padLeft(2, '0')}';
|
|
} catch (_) {
|
|
return '';
|
|
}
|
|
}
|
|
|
|
void _showDeleteConfirm(int id, ColorScheme colors) {
|
|
showDialog(
|
|
context: context,
|
|
builder: (ctx) => AlertDialog(
|
|
title: const Text('删除句读', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600)),
|
|
content: Text('确定删除这条句读?', style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.6))),
|
|
actions: [
|
|
TextButton(onPressed: () => Navigator.pop(ctx), child: Text('取消', style: TextStyle(color: colors.onSurface.withValues(alpha: 0.4)))),
|
|
TextButton(
|
|
onPressed: () { Navigator.pop(ctx); _deleteHighlight(id); },
|
|
child: Text('删除', style: TextStyle(color: colors.error)),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Future<void> _deleteHighlight(int id) async {
|
|
await _dao.deleteHighlight(id);
|
|
_loadHighlights();
|
|
if (mounted) ToastUtil.show(context, '已删除');
|
|
}
|
|
}
|