generated from dellevin/template
支持微信读书导入摘抄
This commit is contained in:
BIN
assets/images/imp_book/wxreader.webp
Normal file
BIN
assets/images/imp_book/wxreader.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.2 KiB |
@@ -3,6 +3,7 @@ import 'package:provider/provider.dart';
|
|||||||
import '../../providers/app_provider.dart';
|
import '../../providers/app_provider.dart';
|
||||||
import '../../models/data_models.dart';
|
import '../../models/data_models.dart';
|
||||||
import '../../utils/toast_util.dart';
|
import '../../utils/toast_util.dart';
|
||||||
|
import 'book_excerpt_share_page.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
/// 摘抄表单页面 - 新增/编辑摘抄
|
/// 摘抄表单页面 - 新增/编辑摘抄
|
||||||
@@ -52,19 +53,9 @@ class _BookExcerptFormPageState extends State<BookExcerptFormPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _loadBookTitle() {
|
void _loadBookTitle() {
|
||||||
setState(() {
|
final provider = context.read<AppProvider>();
|
||||||
_bookTitle = _resolveBookTitle(widget.bookId);
|
final book = provider.books.where((b) => b.id == widget.bookId).firstOrNull;
|
||||||
});
|
if (book != null) setState(() => _bookTitle = book.title);
|
||||||
}
|
|
||||||
|
|
||||||
String _resolveBookTitle(String bookId) {
|
|
||||||
try {
|
|
||||||
final provider = AppProvider();
|
|
||||||
for (final b in provider.books) {
|
|
||||||
if (b.id == bookId) return b.title;
|
|
||||||
}
|
|
||||||
} catch (_) {}
|
|
||||||
return '';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -88,11 +79,18 @@ class _BookExcerptFormPageState extends State<BookExcerptFormPage> {
|
|||||||
padding: EdgeInsets.all(14),
|
padding: EdgeInsets.all(14),
|
||||||
child: SizedBox(width: 18, height: 18, child: CircularProgressIndicator(strokeWidth: 2)),
|
child: SizedBox(width: 18, height: 18, child: CircularProgressIndicator(strokeWidth: 2)),
|
||||||
)
|
)
|
||||||
else
|
else ...[
|
||||||
|
if (_isEditing)
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.ios_share),
|
||||||
|
tooltip: '分享',
|
||||||
|
onPressed: _shareExcerpt,
|
||||||
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: _saveExcerpt,
|
onPressed: _saveExcerpt,
|
||||||
child: Text('保存', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: colors.primary)),
|
child: Text('保存', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: colors.primary)),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -301,4 +299,14 @@ class _BookExcerptFormPageState extends State<BookExcerptFormPage> {
|
|||||||
if (mounted) setState(() => _isLoading = false);
|
if (mounted) setState(() => _isLoading = false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _shareExcerpt() {
|
||||||
|
if (widget.excerpt == null) return;
|
||||||
|
final provider = context.read<AppProvider>();
|
||||||
|
final book = provider.books.where((b) => b.id == widget.bookId).firstOrNull;
|
||||||
|
if (book == null) return;
|
||||||
|
Navigator.push(context, MaterialPageRoute(
|
||||||
|
builder: (_) => BookExcerptSharePage(excerpt: widget.excerpt!, book: book),
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
267
lib/pages/book/book_excerpt_share_page.dart
Normal file
267
lib/pages/book/book_excerpt_share_page.dart
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
import 'dart:ui' as ui;
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/rendering.dart';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
import 'package:share_plus/share_plus.dart';
|
||||||
|
import '../../models/data_models.dart';
|
||||||
|
import '../../utils/toast_util.dart';
|
||||||
|
import '../../widgets/fade_in_local_image.dart';
|
||||||
|
|
||||||
|
/// 摘抄分享海报页面
|
||||||
|
class BookExcerptSharePage extends StatefulWidget {
|
||||||
|
final BookExcerpt excerpt;
|
||||||
|
final Book book;
|
||||||
|
|
||||||
|
const BookExcerptSharePage({super.key, required this.excerpt, required this.book});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<BookExcerptSharePage> createState() => _BookExcerptSharePageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _BookExcerptSharePageState extends State<BookExcerptSharePage> {
|
||||||
|
final GlobalKey _posterKey = GlobalKey();
|
||||||
|
bool _isGenerating = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: colors.surfaceContainerHighest,
|
||||||
|
appBar: AppBar(
|
||||||
|
backgroundColor: colors.surface,
|
||||||
|
elevation: 0,
|
||||||
|
leading: IconButton(
|
||||||
|
icon: Icon(Icons.close, color: colors.onSurface),
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
'分享摘抄',
|
||||||
|
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: colors.onSurface),
|
||||||
|
),
|
||||||
|
centerTitle: true,
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: _isGenerating ? null : _generateAndShare,
|
||||||
|
child: _isGenerating
|
||||||
|
? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2))
|
||||||
|
: Text('分享', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: colors.onSurface)),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: Center(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
child: RepaintBoundary(
|
||||||
|
key: _posterKey,
|
||||||
|
child: _buildPosterWidget(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildPosterWidget() {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
|
final excerpt = widget.excerpt;
|
||||||
|
final book = widget.book;
|
||||||
|
final hasCover = book.coverPath != null && book.coverPath!.isNotEmpty;
|
||||||
|
final dateStr = '${excerpt.createdAt.year}.${excerpt.createdAt.month.toString().padLeft(2, '0')}.${excerpt.createdAt.day.toString().padLeft(2, '0')}';
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
width: 320,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: colors.surface,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(color: Colors.black.withValues(alpha: 0.1), blurRadius: 20, offset: const Offset(0, 10)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// 顶部:书籍信息条
|
||||||
|
if (hasCover || book.title.isNotEmpty)
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.fromLTRB(20, 20, 20, 0),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
// 小封面
|
||||||
|
if (hasCover)
|
||||||
|
Container(
|
||||||
|
width: 36,
|
||||||
|
height: 50,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
color: colors.surfaceContainerHighest,
|
||||||
|
),
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
child: FadeInLocalImage(
|
||||||
|
path: book.coverPath,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
errorWidget: const SizedBox.shrink(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (hasCover) const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
book.title,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: colors.onSurface),
|
||||||
|
),
|
||||||
|
if (book.authors.isNotEmpty) ...[
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
Text(
|
||||||
|
book.authors.join(' / '),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// 分隔线
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
|
||||||
|
child: Container(height: 0.5, color: colors.outline),
|
||||||
|
),
|
||||||
|
|
||||||
|
// 摘抄内容
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// 想法
|
||||||
|
if (excerpt.comment.isNotEmpty) ...[
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.chat_bubble_outline, size: 14, color: colors.primary.withValues(alpha: 0.6)),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
excerpt.comment,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
color: colors.onSurface.withValues(alpha: 0.5),
|
||||||
|
height: 1.7,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 14),
|
||||||
|
],
|
||||||
|
// 引号 + 内容
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'\u201C',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 28,
|
||||||
|
color: colors.primary.withValues(alpha: 0.4),
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
height: 1.1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
excerpt.content,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 15,
|
||||||
|
color: colors.onSurface.withValues(alpha: 0.85),
|
||||||
|
height: 1.9,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// 底部:章节 + 日期 + 品牌
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(20, 20, 20, 20),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Container(height: 0.5, color: colors.outline),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
if (excerpt.chapter.isNotEmpty) ...[
|
||||||
|
Flexible(
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: colors.primary.withValues(alpha: 0.08),
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
excerpt.chapter,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: TextStyle(fontSize: 11, color: colors.primary.withValues(alpha: 0.7)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
],
|
||||||
|
Text(dateStr, style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.3))),
|
||||||
|
const Spacer(),
|
||||||
|
Text(
|
||||||
|
'MookNote',
|
||||||
|
style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.25), fontWeight: FontWeight.w500),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _generateAndShare() async {
|
||||||
|
setState(() => _isGenerating = true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
final boundary = _posterKey.currentContext?.findRenderObject() as RenderRepaintBoundary?;
|
||||||
|
if (boundary == null) throw Exception('无法获取海报边界');
|
||||||
|
|
||||||
|
final image = await boundary.toImage(pixelRatio: 3.0);
|
||||||
|
final byteData = await image.toByteData(format: ui.ImageByteFormat.png);
|
||||||
|
if (byteData == null) throw Exception('无法生成图片数据');
|
||||||
|
|
||||||
|
final bytes = byteData.buffer.asUint8List();
|
||||||
|
final tempDir = await getTemporaryDirectory();
|
||||||
|
final fileName = 'excerpt_share_${DateTime.now().millisecondsSinceEpoch}.png';
|
||||||
|
final file = File('${tempDir.path}/$fileName');
|
||||||
|
await file.writeAsBytes(bytes);
|
||||||
|
|
||||||
|
await Share.shareXFiles(
|
||||||
|
[XFile(file.path)],
|
||||||
|
text: '分享摘抄:${widget.book.title}',
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) ToastUtil.show(context, '生成海报失败:$e');
|
||||||
|
} finally {
|
||||||
|
if (mounted) setState(() => _isGenerating = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import '../../providers/app_provider.dart';
|
import '../../providers/app_provider.dart';
|
||||||
import '../../models/data_models.dart';
|
import '../../models/data_models.dart';
|
||||||
import '../../utils/toast_util.dart';
|
import '../../utils/toast_util.dart';
|
||||||
import '../../utils/epub/reader_dao.dart';
|
import '../../utils/epub/reader_dao.dart';
|
||||||
|
import '../../widgets/fade_in_local_image.dart';
|
||||||
import 'book_excerpt_form_page.dart';
|
import 'book_excerpt_form_page.dart';
|
||||||
|
|
||||||
/// 书籍摘抄列表页面
|
/// 书籍摘抄列表页面
|
||||||
@@ -42,31 +44,124 @@ class _BookExcerptsPageState extends State<BookExcerptsPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int get _thoughtCount => _excerpts.where((e) => e.comment.isNotEmpty).length;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final colors = Theme.of(context).colorScheme;
|
final colors = Theme.of(context).colorScheme;
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: colors.surface,
|
backgroundColor: colors.surface,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Column(
|
title: const Text('摘抄', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600)),
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
actions: [
|
||||||
children: [
|
IconButton(
|
||||||
const Text('摘抄', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600)),
|
icon: const Icon(Icons.note_add_outlined),
|
||||||
if (_excerpts.isNotEmpty)
|
tooltip: '导入',
|
||||||
Text('共 ${_excerpts.length} 条', style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.4))),
|
onPressed: _showImportSheet,
|
||||||
],
|
),
|
||||||
),
|
IconButton(
|
||||||
|
icon: const Icon(Icons.delete_sweep_outlined),
|
||||||
|
tooltip: '删除全部',
|
||||||
|
onPressed: _excerpts.isEmpty ? null : _showDeleteAllDialog,
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
|
body: _isLoading
|
||||||
|
? Center(child: CircularProgressIndicator(color: colors.primary))
|
||||||
|
: Column(
|
||||||
|
children: [
|
||||||
|
_buildBookHeader(colors),
|
||||||
|
Expanded(
|
||||||
|
child: _excerpts.isEmpty
|
||||||
|
? _buildEmptyState(colors)
|
||||||
|
: _buildExcerptList(colors),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
floatingActionButton: FloatingActionButton.extended(
|
floatingActionButton: FloatingActionButton.extended(
|
||||||
onPressed: _navigateToAddExcerpt,
|
onPressed: _navigateToAddExcerpt,
|
||||||
icon: const Icon(Icons.add, size: 20),
|
icon: const Icon(Icons.add, size: 20),
|
||||||
label: const Text('添加摘抄'),
|
label: const Text('添加摘抄'),
|
||||||
),
|
),
|
||||||
body: _isLoading
|
);
|
||||||
? Center(child: CircularProgressIndicator(color: colors.primary))
|
}
|
||||||
: _excerpts.isEmpty
|
|
||||||
? _buildEmptyState(colors)
|
// ── 书籍头部 ──
|
||||||
: _buildExcerptList(colors),
|
|
||||||
|
Widget _buildBookHeader(ColorScheme colors) {
|
||||||
|
final book = widget.book;
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.fromLTRB(20, 12, 20, 16),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// 封面
|
||||||
|
Container(
|
||||||
|
width: 52,
|
||||||
|
height: 70,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
color: colors.surfaceContainerHighest,
|
||||||
|
),
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
child: book.coverPath != null && book.coverPath!.isNotEmpty
|
||||||
|
? FadeInLocalImage(
|
||||||
|
path: book.coverPath,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
errorWidget: Icon(Icons.menu_book_outlined, size: 22, color: colors.onSurface.withValues(alpha: 0.25)),
|
||||||
|
)
|
||||||
|
: Icon(Icons.menu_book_outlined, size: 22, color: colors.onSurface.withValues(alpha: 0.25)),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 14),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
book.title,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: colors.onSurface),
|
||||||
|
),
|
||||||
|
if (book.authors.isNotEmpty) ...[
|
||||||
|
const SizedBox(height: 3),
|
||||||
|
Text(
|
||||||
|
book.authors.take(2).join('、'),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.4)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
// 统计行
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
_buildStatChip(colors, Icons.format_quote, '${_excerpts.length} 条摘抄'),
|
||||||
|
if (_thoughtCount > 0) ...[
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
_buildStatChip(colors, Icons.chat_bubble_outline, '$_thoughtCount 条想法'),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildStatChip(ColorScheme colors, IconData icon, String label) {
|
||||||
|
return Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(icon, size: 14, color: colors.onSurface.withValues(alpha: 0.35)),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.45)),
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,7 +198,7 @@ class _BookExcerptsPageState extends State<BookExcerptsPage> {
|
|||||||
Widget _buildExcerptList(ColorScheme colors) {
|
Widget _buildExcerptList(ColorScheme colors) {
|
||||||
final chapters = _groupExcerptsByChapter().keys.toList();
|
final chapters = _groupExcerptsByChapter().keys.toList();
|
||||||
return ListView.builder(
|
return ListView.builder(
|
||||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 80),
|
padding: const EdgeInsets.fromLTRB(16, 4, 16, 80),
|
||||||
itemCount: chapters.length,
|
itemCount: chapters.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final chapter = chapters[index];
|
final chapter = chapters[index];
|
||||||
@@ -130,7 +225,7 @@ class _BookExcerptsPageState extends State<BookExcerptsPage> {
|
|||||||
|
|
||||||
Widget _buildChapterSection(String chapter, List<BookExcerpt> excerpts, ColorScheme colors, int chapterIndex) {
|
Widget _buildChapterSection(String chapter, List<BookExcerpt> excerpts, ColorScheme colors, int chapterIndex) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: EdgeInsets.only(bottom: 20),
|
padding: const EdgeInsets.only(bottom: 20),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
@@ -177,49 +272,55 @@ class _BookExcerptsPageState extends State<BookExcerptsPage> {
|
|||||||
// ── 摘抄卡片 ──
|
// ── 摘抄卡片 ──
|
||||||
|
|
||||||
Widget _buildExcerptCard(BookExcerpt excerpt, ColorScheme colors) {
|
Widget _buildExcerptCard(BookExcerpt excerpt, ColorScheme colors) {
|
||||||
return Dismissible(
|
return Material(
|
||||||
key: ValueKey(excerpt.id),
|
color: colors.surfaceContainerHigh,
|
||||||
direction: DismissDirection.endToStart,
|
borderRadius: BorderRadius.circular(14),
|
||||||
background: Container(
|
clipBehavior: Clip.antiAlias,
|
||||||
alignment: Alignment.centerRight,
|
child: InkWell(
|
||||||
padding: const EdgeInsets.only(right: 20),
|
onTap: () => _navigateToEditExcerpt(excerpt),
|
||||||
decoration: BoxDecoration(
|
onLongPress: () => _showDeleteDialog(excerpt),
|
||||||
color: colors.error,
|
child: Container(
|
||||||
borderRadius: BorderRadius.circular(14),
|
|
||||||
),
|
|
||||||
child: const Icon(Icons.delete_outline, color: Colors.white),
|
|
||||||
),
|
|
||||||
confirmDismiss: (direction) => _showDeleteDialog(excerpt),
|
|
||||||
child: Material(
|
|
||||||
color: colors.surfaceContainerHigh,
|
|
||||||
borderRadius: BorderRadius.circular(14),
|
|
||||||
clipBehavior: Clip.antiAlias,
|
|
||||||
child: InkWell(
|
|
||||||
onTap: () => _navigateToEditExcerpt(excerpt),
|
|
||||||
child: Container(
|
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
padding: const EdgeInsets.fromLTRB(16, 14, 14, 12),
|
padding: const EdgeInsets.fromLTRB(16, 14, 14, 12),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// 引号 + 摘抄内容
|
// 想法(在内容上方,无背景)
|
||||||
Stack(
|
if (excerpt.comment.isNotEmpty) ...[
|
||||||
children: [
|
Row(
|
||||||
Positioned(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
left: -6,
|
children: [
|
||||||
top: -6,
|
Icon(Icons.chat_bubble_outline, size: 13, color: colors.primary.withValues(alpha: 0.5)),
|
||||||
child: Text(
|
const SizedBox(width: 6),
|
||||||
'"',
|
Expanded(
|
||||||
style: TextStyle(
|
child: Text(
|
||||||
fontSize: 32,
|
excerpt.comment,
|
||||||
color: colors.primary.withValues(alpha: 0.15),
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.bold,
|
fontSize: 12,
|
||||||
height: 1,
|
color: colors.onSurface.withValues(alpha: 0.5),
|
||||||
|
height: 1.6,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
],
|
||||||
|
// 摘抄内容
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'\u201C',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 20,
|
||||||
|
color: colors.primary.withValues(alpha: 0.35),
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
height: 1.2,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
Padding(
|
const SizedBox(width: 4),
|
||||||
padding: const EdgeInsets.only(left: 10),
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
excerpt.content,
|
excerpt.content,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
@@ -231,30 +332,7 @@ class _BookExcerptsPageState extends State<BookExcerptsPage> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
// 感悟
|
// 底部:日期
|
||||||
if (excerpt.comment.isNotEmpty) ...[
|
|
||||||
const SizedBox(height: 10),
|
|
||||||
Container(
|
|
||||||
width: double.infinity,
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: colors.surface,
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
border: Border(
|
|
||||||
left: BorderSide(color: colors.primary.withValues(alpha: 0.3), width: 2),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
excerpt.comment,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
color: colors.onSurface.withValues(alpha: 0.55),
|
|
||||||
height: 1.6,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
// 底部:日期 + 操作
|
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
@@ -264,22 +342,13 @@ class _BookExcerptsPageState extends State<BookExcerptsPage> {
|
|||||||
_formatDate(excerpt.createdAt),
|
_formatDate(excerpt.createdAt),
|
||||||
style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.3)),
|
style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.3)),
|
||||||
),
|
),
|
||||||
const Spacer(),
|
|
||||||
GestureDetector(
|
|
||||||
onTap: () => _navigateToEditExcerpt(excerpt),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
|
||||||
child: Icon(Icons.edit_outlined, size: 16, color: colors.onSurface.withValues(alpha: 0.35)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
String _formatDate(DateTime date) => '${date.year}.${date.month.toString().padLeft(2, '0')}.${date.day.toString().padLeft(2, '0')}';
|
String _formatDate(DateTime date) => '${date.year}.${date.month.toString().padLeft(2, '0')}.${date.day.toString().padLeft(2, '0')}';
|
||||||
@@ -300,12 +369,16 @@ class _BookExcerptsPageState extends State<BookExcerptsPage> {
|
|||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
backgroundColor: colors.surface,
|
backgroundColor: colors.surface,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
title: const Text('删除摘抄', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600)),
|
title: Text('确认删除', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: colors.onSurface)),
|
||||||
content: Text('确定删除这条摘抄吗?', style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.6))),
|
content: Text('确定删除这条摘抄吗?', style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.6), height: 1.5)),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(onPressed: () => Navigator.pop(context, false), child: Text('取消', style: TextStyle(color: colors.onSurface.withValues(alpha: 0.4)))),
|
|
||||||
TextButton(
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context, false),
|
||||||
|
style: TextButton.styleFrom(foregroundColor: colors.onSurface.withValues(alpha: 0.6), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8)),
|
||||||
|
child: const Text('取消'),
|
||||||
|
),
|
||||||
|
ElevatedButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
Navigator.pop(context, true);
|
Navigator.pop(context, true);
|
||||||
final readerBook = await ReaderDao().getReaderBookByBookId(widget.book.id);
|
final readerBook = await ReaderDao().getReaderBookByBookId(widget.book.id);
|
||||||
@@ -315,15 +388,373 @@ class _BookExcerptsPageState extends State<BookExcerptsPage> {
|
|||||||
excerpt.content,
|
excerpt.content,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
await context.read<AppProvider>().removeBookExcerpt(excerpt.id);
|
await this.context.read<AppProvider>().removeBookExcerpt(excerpt.id);
|
||||||
_loadExcerpts();
|
_loadExcerpts();
|
||||||
if (mounted) ToastUtil.show(context, '已删除');
|
if (mounted) ToastUtil.show(this.context, '已删除');
|
||||||
},
|
},
|
||||||
child: Text('删除', style: TextStyle(color: colors.error, fontWeight: FontWeight.w600)),
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: colors.error, foregroundColor: colors.onError, elevation: 0,
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
|
),
|
||||||
|
child: const Text('删除'),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _showDeleteAllDialog() {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) {
|
||||||
|
return AlertDialog(
|
||||||
|
backgroundColor: colors.surface,
|
||||||
|
elevation: 0,
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
|
title: Text('确认删除', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: colors.onSurface)),
|
||||||
|
content: Text('确定删除全部 ${_excerpts.length} 条摘抄吗?此操作不可恢复。', style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.6), height: 1.5)),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
style: TextButton.styleFrom(foregroundColor: colors.onSurface.withValues(alpha: 0.6), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8)),
|
||||||
|
child: const Text('取消'),
|
||||||
|
),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () async {
|
||||||
|
Navigator.pop(context);
|
||||||
|
final provider = this.context.read<AppProvider>();
|
||||||
|
for (final excerpt in _excerpts) {
|
||||||
|
await provider.removeBookExcerpt(excerpt.id);
|
||||||
|
}
|
||||||
|
_loadExcerpts();
|
||||||
|
if (mounted) ToastUtil.show(this.context, '已删除全部摘抄');
|
||||||
|
},
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: colors.error, foregroundColor: colors.onError, elevation: 0,
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
|
),
|
||||||
|
child: const Text('删除全部'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 导入 ──
|
||||||
|
|
||||||
|
void _showImportSheet() {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
isScrollControlled: true,
|
||||||
|
builder: (context) {
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: colors.surface,
|
||||||
|
borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),
|
||||||
|
),
|
||||||
|
child: SafeArea(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 40,
|
||||||
|
height: 4,
|
||||||
|
margin: const EdgeInsets.only(top: 12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: colors.outline,
|
||||||
|
borderRadius: BorderRadius.circular(2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Text('导入摘抄', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: colors.onSurface)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
// 微信读书
|
||||||
|
InkWell(
|
||||||
|
onTap: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
_showWechatReadImport();
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 44,
|
||||||
|
height: 44,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFF07C160).withValues(alpha: 0.1),
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
child: Image.asset('assets/images/imp_book/wxreader.webp', width: 44, height: 44),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text('微信读书', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500, color: colors.onSurface)),
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
Text('粘贴微信读书导出的笔记', style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.4))),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Icon(Icons.chevron_right, color: colors.onSurface.withValues(alpha: 0.25), size: 20),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showWechatReadImport() {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
isScrollControlled: true,
|
||||||
|
builder: (sheetContext) {
|
||||||
|
final controller = TextEditingController();
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.only(bottom: MediaQuery.of(sheetContext).viewInsets.bottom),
|
||||||
|
child: Container(
|
||||||
|
constraints: BoxConstraints(maxHeight: MediaQuery.of(sheetContext).size.height * 0.75),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: colors.surface,
|
||||||
|
borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),
|
||||||
|
),
|
||||||
|
child: SafeArea(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 40,
|
||||||
|
height: 4,
|
||||||
|
margin: const EdgeInsets.only(top: 12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: colors.outline,
|
||||||
|
borderRadius: BorderRadius.circular(2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Text('微信读书导入', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: colors.onSurface)),
|
||||||
|
const Spacer(),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () async {
|
||||||
|
final data = await Clipboard.getData('text/plain');
|
||||||
|
if (data?.text != null) {
|
||||||
|
controller.text = data!.text!;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Text('粘贴', style: TextStyle(color: colors.primary, fontSize: 14)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
child: TextField(
|
||||||
|
controller: controller,
|
||||||
|
maxLines: 8,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: '粘贴微信读书导出的笔记内容...',
|
||||||
|
hintStyle: TextStyle(color: colors.onSurface.withValues(alpha: 0.25)),
|
||||||
|
filled: true,
|
||||||
|
fillColor: colors.surfaceContainerHighest,
|
||||||
|
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none),
|
||||||
|
),
|
||||||
|
style: TextStyle(fontSize: 14, color: colors.onSurface),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
child: SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: () {
|
||||||
|
final text = controller.text.trim();
|
||||||
|
if (text.isEmpty) {
|
||||||
|
ToastUtil.show(sheetContext, '请粘贴内容');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Navigator.pop(sheetContext);
|
||||||
|
_importWechatRead(text);
|
||||||
|
},
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: colors.primary,
|
||||||
|
foregroundColor: colors.onPrimary,
|
||||||
|
elevation: 0,
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||||
|
),
|
||||||
|
child: const Text('导入', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _importWechatRead(String text) {
|
||||||
|
final excerpts = _parseWechatRead(text);
|
||||||
|
if (excerpts.isEmpty) {
|
||||||
|
ToastUtil.show(context, '未识别到有效摘抄');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_saveImportedExcerpts(excerpts);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 解析微信读书导出格式
|
||||||
|
List<({String content, String comment, String chapter})> _parseWechatRead(String text) {
|
||||||
|
final lines = text.split(RegExp(r'\r?\n'));
|
||||||
|
final results = <({String content, String comment, String chapter})>[];
|
||||||
|
String? currentComment;
|
||||||
|
bool waitingForOriginal = false;
|
||||||
|
String currentChapter = '';
|
||||||
|
|
||||||
|
for (int i = 0; i < lines.length; i++) {
|
||||||
|
final line = lines[i].trim();
|
||||||
|
|
||||||
|
// 跳过空行和结尾
|
||||||
|
if (line.isEmpty || line.startsWith('-- 来自微信读书')) continue;
|
||||||
|
|
||||||
|
// 跳过书名行(以《开头)和笔记数量行
|
||||||
|
if (line.startsWith('《') || RegExp(r'^\d+个笔记$').hasMatch(line)) continue;
|
||||||
|
|
||||||
|
// 匹配 "◆ YYYY/MM/DD发表想法"
|
||||||
|
final thoughtMatch = RegExp(r'^◆\s*\d{4}/\d{2}/\d{2}发表想法').firstMatch(line);
|
||||||
|
if (thoughtMatch != null) {
|
||||||
|
// 如果之前有未匹配原文的想法,先保存
|
||||||
|
if (currentComment != null && !waitingForOriginal) {
|
||||||
|
results.add((content: currentComment, comment: '', chapter: currentChapter));
|
||||||
|
currentComment = null;
|
||||||
|
}
|
||||||
|
// 想法行本身可能有内容在 "发表想法" 之后
|
||||||
|
final afterThought = line.substring(thoughtMatch.end).trim();
|
||||||
|
currentComment = afterThought.isNotEmpty ? afterThought : '';
|
||||||
|
waitingForOriginal = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 匹配 "原文:..."
|
||||||
|
if (line.startsWith('原文:') || line.startsWith('原文:')) {
|
||||||
|
final originalText = line.substring(3).trim();
|
||||||
|
if (originalText.isNotEmpty) {
|
||||||
|
results.add((content: originalText, comment: currentComment ?? '', chapter: currentChapter));
|
||||||
|
currentComment = null;
|
||||||
|
waitingForOriginal = false;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 匹配普通划线 "◆ ..."
|
||||||
|
if (line.startsWith('◆ ')) {
|
||||||
|
// 如果之前有未匹配原文的想法,先保存想法本身
|
||||||
|
if (currentComment != null && waitingForOriginal) {
|
||||||
|
if (currentComment.isNotEmpty) {
|
||||||
|
results.add((content: currentComment, comment: '', chapter: currentChapter));
|
||||||
|
}
|
||||||
|
currentComment = null;
|
||||||
|
waitingForOriginal = false;
|
||||||
|
}
|
||||||
|
final content = line.substring(2).trim();
|
||||||
|
if (content.isNotEmpty) {
|
||||||
|
results.add((content: content, comment: '', chapter: currentChapter));
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 多行想法内容(想法紧接在 "发表想法" 行之后)
|
||||||
|
if (currentComment != null && waitingForOriginal && line.isNotEmpty) {
|
||||||
|
// 检查是否是 "原文:" 行
|
||||||
|
if (line.startsWith('原文:') || line.startsWith('原文:')) {
|
||||||
|
final originalText = line.substring(3).trim();
|
||||||
|
if (originalText.isNotEmpty) {
|
||||||
|
results.add((content: originalText, comment: currentComment, chapter: currentChapter));
|
||||||
|
}
|
||||||
|
currentComment = null;
|
||||||
|
waitingForOriginal = false;
|
||||||
|
} else {
|
||||||
|
// 追加到想法内容
|
||||||
|
currentComment = currentComment.isEmpty ? line : '$currentComment\n$line';
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 其他非空行视为章节标题
|
||||||
|
if (line.isNotEmpty) {
|
||||||
|
currentChapter = line;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理末尾残留
|
||||||
|
if (currentComment != null && currentComment.isNotEmpty) {
|
||||||
|
results.add((content: currentComment, comment: '', chapter: currentChapter));
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _saveImportedExcerpts(List<({String content, String comment, String chapter})> excerpts) async {
|
||||||
|
final provider = context.read<AppProvider>();
|
||||||
|
final now = DateTime.now();
|
||||||
|
int imported = 0;
|
||||||
|
|
||||||
|
for (int i = 0; i < excerpts.length; i++) {
|
||||||
|
final item = excerpts[i];
|
||||||
|
final excerpt = BookExcerpt(
|
||||||
|
id: '${widget.book.id}_${now.millisecondsSinceEpoch}_$i',
|
||||||
|
bookId: widget.book.id,
|
||||||
|
chapter: item.chapter,
|
||||||
|
content: item.content,
|
||||||
|
comment: item.comment,
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
);
|
||||||
|
await provider.addBookExcerpt(excerpt);
|
||||||
|
imported++;
|
||||||
|
}
|
||||||
|
|
||||||
|
_loadExcerpts();
|
||||||
|
if (mounted) {
|
||||||
|
ToastUtil.show(context, '成功导入 $imported 条摘抄');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -456,9 +456,9 @@ class _EpubDetailPageState extends State<EpubDetailPage> {
|
|||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.menu_book_outlined, size: 14, color: colors.primary.withValues(alpha: 0.6)),
|
Icon(Icons.menu_book_outlined, size: 14, color: colors.primary.withValues(alpha: 0.6)),
|
||||||
const SizedBox(width: 6),
|
const Spacer(),
|
||||||
if (excerpt.chapter.isNotEmpty)
|
if (excerpt.chapter.isNotEmpty)
|
||||||
Expanded(
|
Flexible(
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@@ -469,7 +469,6 @@ class _EpubDetailPageState extends State<EpubDetailPage> {
|
|||||||
excerpt.chapter,
|
excerpt.chapter,
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
textAlign: TextAlign.right,
|
|
||||||
style: TextStyle(fontSize: 9, color: colors.primary.withValues(alpha: 0.7), fontWeight: FontWeight.w500),
|
style: TextStyle(fontSize: 9, color: colors.primary.withValues(alpha: 0.7), fontWeight: FontWeight.w500),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -270,13 +270,25 @@ class _EpubHighlightsPageState extends State<EpubHighlightsPage> {
|
|||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (ctx) => AlertDialog(
|
builder: (ctx) => AlertDialog(
|
||||||
title: const Text('删除句读', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600)),
|
backgroundColor: colors.surface,
|
||||||
content: Text('确定删除这条句读?', style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.6))),
|
elevation: 0,
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
|
title: Text('确认删除', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: colors.onSurface)),
|
||||||
|
content: Text('确定删除这条句读?', style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.6), height: 1.5)),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(onPressed: () => Navigator.pop(ctx), child: Text('取消', style: TextStyle(color: colors.onSurface.withValues(alpha: 0.4)))),
|
|
||||||
TextButton(
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(ctx),
|
||||||
|
style: TextButton.styleFrom(foregroundColor: colors.onSurface.withValues(alpha: 0.6), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8)),
|
||||||
|
child: const Text('取消'),
|
||||||
|
),
|
||||||
|
ElevatedButton(
|
||||||
onPressed: () { Navigator.pop(ctx); _deleteHighlight(id); },
|
onPressed: () { Navigator.pop(ctx); _deleteHighlight(id); },
|
||||||
child: Text('删除', style: TextStyle(color: colors.error)),
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: colors.error, foregroundColor: colors.onError, elevation: 0,
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
|
),
|
||||||
|
child: const Text('删除'),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import '../../utils/toast_util.dart';
|
import '../../utils/toast_util.dart';
|
||||||
|
import 'highlight_share_page.dart';
|
||||||
|
|
||||||
/// 句读详情弹窗 —— 类似分享卡片的样式
|
/// 句读详情弹窗 —— 类似分享卡片的样式
|
||||||
/// 从 epub_detail_page 和 epub_highlights_page 共用
|
/// 从 epub_detail_page 和 epub_highlights_page 共用
|
||||||
@@ -50,7 +51,7 @@ void showHighlightDetailSheet(
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(24, 24, 24, 24),
|
padding: const EdgeInsets.fromLTRB(24, 20, 24, 20),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
@@ -128,13 +129,30 @@ void showHighlightDetailSheet(
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 20),
|
||||||
// 分隔线
|
|
||||||
Container(height: 0.5, color: colors.outlineVariant),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
// 操作按钮
|
// 操作按钮
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
|
_buildActionButton(
|
||||||
|
colors,
|
||||||
|
icon: Icons.ios_share_outlined,
|
||||||
|
label: '分享',
|
||||||
|
onTap: () {
|
||||||
|
Navigator.pop(ctx);
|
||||||
|
String chapterLabel = '';
|
||||||
|
if (chapterNum != null) chapterLabel = '第${chapterNum + 1}章';
|
||||||
|
String dateLabel = '';
|
||||||
|
if (createdAt.isNotEmpty) dateLabel = _formatDate(createdAt);
|
||||||
|
Navigator.push(context, MaterialPageRoute(
|
||||||
|
builder: (_) => HighlightSharePage(
|
||||||
|
content: content,
|
||||||
|
bookTitle: bookTitle,
|
||||||
|
chapter: chapterLabel,
|
||||||
|
dateStr: dateLabel,
|
||||||
|
),
|
||||||
|
));
|
||||||
|
},
|
||||||
|
),
|
||||||
_buildActionButton(
|
_buildActionButton(
|
||||||
colors,
|
colors,
|
||||||
icon: Icons.content_copy_outlined,
|
icon: Icons.content_copy_outlined,
|
||||||
@@ -264,7 +282,7 @@ void showExcerptDetailSheet(
|
|||||||
),
|
),
|
||||||
Flexible(
|
Flexible(
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
padding: const EdgeInsets.fromLTRB(24, 24, 24, 24),
|
padding: const EdgeInsets.fromLTRB(24, 20, 24, 20),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
@@ -330,24 +348,22 @@ void showExcerptDetailSheet(
|
|||||||
// 感悟
|
// 感悟
|
||||||
if (comment.isNotEmpty) ...[
|
if (comment.isNotEmpty) ...[
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Container(
|
Row(
|
||||||
width: double.infinity,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
children: [
|
||||||
decoration: BoxDecoration(
|
Icon(Icons.chat_bubble_outline, size: 14, color: colors.primary.withValues(alpha: 0.5)),
|
||||||
color: colors.surfaceContainerHigh,
|
const SizedBox(width: 6),
|
||||||
borderRadius: BorderRadius.circular(10),
|
Expanded(
|
||||||
border: Border(
|
child: Text(
|
||||||
left: BorderSide(color: colors.primary.withValues(alpha: 0.4), width: 2),
|
comment,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
color: colors.onSurface.withValues(alpha: 0.5),
|
||||||
|
height: 1.6,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
child: Text(
|
|
||||||
comment,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 13,
|
|
||||||
color: colors.onSurface.withValues(alpha: 0.6),
|
|
||||||
height: 1.6,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
@@ -362,13 +378,26 @@ void showExcerptDetailSheet(
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 20),
|
||||||
// 分隔线
|
|
||||||
Container(height: 0.5, color: colors.outlineVariant),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
// 操作按钮
|
// 操作按钮
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
|
_buildActionButton(
|
||||||
|
colors,
|
||||||
|
icon: Icons.ios_share_outlined,
|
||||||
|
label: '分享',
|
||||||
|
onTap: () {
|
||||||
|
Navigator.pop(ctx);
|
||||||
|
Navigator.push(context, MaterialPageRoute(
|
||||||
|
builder: (_) => HighlightSharePage(
|
||||||
|
content: content,
|
||||||
|
bookTitle: bookTitle,
|
||||||
|
chapter: chapter,
|
||||||
|
dateStr: _formatDateFromDateTime(createdAt),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
},
|
||||||
|
),
|
||||||
_buildActionButton(
|
_buildActionButton(
|
||||||
colors,
|
colors,
|
||||||
icon: Icons.content_copy_outlined,
|
icon: Icons.content_copy_outlined,
|
||||||
|
|||||||
209
lib/pages/epub_reader/highlight_share_page.dart
Normal file
209
lib/pages/epub_reader/highlight_share_page.dart
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
import 'dart:ui' as ui;
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/rendering.dart';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
import 'package:share_plus/share_plus.dart';
|
||||||
|
import '../../utils/toast_util.dart';
|
||||||
|
|
||||||
|
/// 句读分享海报页面
|
||||||
|
class HighlightSharePage extends StatefulWidget {
|
||||||
|
final String content;
|
||||||
|
final String bookTitle;
|
||||||
|
final String chapter;
|
||||||
|
final String dateStr;
|
||||||
|
|
||||||
|
const HighlightSharePage({
|
||||||
|
super.key,
|
||||||
|
required this.content,
|
||||||
|
required this.bookTitle,
|
||||||
|
this.chapter = '',
|
||||||
|
this.dateStr = '',
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<HighlightSharePage> createState() => _HighlightSharePageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _HighlightSharePageState extends State<HighlightSharePage> {
|
||||||
|
final GlobalKey _posterKey = GlobalKey();
|
||||||
|
bool _isGenerating = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: colors.surfaceContainerHighest,
|
||||||
|
appBar: AppBar(
|
||||||
|
backgroundColor: colors.surface,
|
||||||
|
elevation: 0,
|
||||||
|
leading: IconButton(
|
||||||
|
icon: Icon(Icons.close, color: colors.onSurface),
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
'分享句读',
|
||||||
|
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: colors.onSurface),
|
||||||
|
),
|
||||||
|
centerTitle: true,
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: _isGenerating ? null : _generateAndShare,
|
||||||
|
child: _isGenerating
|
||||||
|
? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2))
|
||||||
|
: Text('分享', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: colors.onSurface)),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: Center(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
child: RepaintBoundary(
|
||||||
|
key: _posterKey,
|
||||||
|
child: _buildPosterWidget(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildPosterWidget() {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
width: 320,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: colors.surface,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(color: Colors.black.withValues(alpha: 0.1), blurRadius: 20, offset: const Offset(0, 10)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// 顶部:书名
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(20, 20, 20, 0),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.book_outlined, size: 16, color: const Color(0xFFFFC107)),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
widget.bookTitle,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: colors.onSurface),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// 分隔线
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
|
||||||
|
child: Container(height: 0.5, color: colors.outline),
|
||||||
|
),
|
||||||
|
|
||||||
|
// 引号 + 内容
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'\u201C',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 28,
|
||||||
|
color: const Color(0xFFFFC107).withValues(alpha: 0.4),
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
height: 1.1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
widget.content,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 15,
|
||||||
|
color: colors.onSurface.withValues(alpha: 0.85),
|
||||||
|
height: 1.9,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// 底部:章节 + 日期 + 品牌
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(20, 20, 20, 20),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Container(height: 0.5, color: colors.outline),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
if (widget.chapter.isNotEmpty) ...[
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFFFFEB3B).withValues(alpha: 0.18),
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
widget.chapter,
|
||||||
|
style: const TextStyle(fontSize: 11, color: Color(0xFF795548), fontWeight: FontWeight.w500),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
],
|
||||||
|
if (widget.dateStr.isNotEmpty)
|
||||||
|
Text(widget.dateStr, style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.3))),
|
||||||
|
const Spacer(),
|
||||||
|
Text(
|
||||||
|
'MookNote',
|
||||||
|
style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.25), fontWeight: FontWeight.w500),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _generateAndShare() async {
|
||||||
|
setState(() => _isGenerating = true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
final boundary = _posterKey.currentContext?.findRenderObject() as RenderRepaintBoundary?;
|
||||||
|
if (boundary == null) throw Exception('无法获取海报边界');
|
||||||
|
|
||||||
|
final image = await boundary.toImage(pixelRatio: 3.0);
|
||||||
|
final byteData = await image.toByteData(format: ui.ImageByteFormat.png);
|
||||||
|
if (byteData == null) throw Exception('无法生成图片数据');
|
||||||
|
|
||||||
|
final bytes = byteData.buffer.asUint8List();
|
||||||
|
final tempDir = await getTemporaryDirectory();
|
||||||
|
final fileName = 'highlight_share_${DateTime.now().millisecondsSinceEpoch}.png';
|
||||||
|
final file = File('${tempDir.path}/$fileName');
|
||||||
|
await file.writeAsBytes(bytes);
|
||||||
|
|
||||||
|
await Share.shareXFiles(
|
||||||
|
[XFile(file.path)],
|
||||||
|
text: '分享句读:${widget.bookTitle}',
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) ToastUtil.show(context, '生成海报失败:$e');
|
||||||
|
} finally {
|
||||||
|
if (mounted) setState(() => _isGenerating = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -588,7 +588,12 @@ class _WebDAVSyncPageState extends State<WebDAVSyncPage> {
|
|||||||
Text('云端备份', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: colors.onSurface)),
|
Text('云端备份', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: colors.onSurface)),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
if (_isLoadingRemoteInfo)
|
if (_isLoadingRemoteInfo)
|
||||||
SizedBox(width: 14, height: 14, child: CircularProgressIndicator(strokeWidth: 2, color: colors.primary)),
|
SizedBox(width: 14, height: 14, child: CircularProgressIndicator(strokeWidth: 2, color: colors.primary))
|
||||||
|
else
|
||||||
|
GestureDetector(
|
||||||
|
onTap: _loadRemoteInfo,
|
||||||
|
child: Icon(Icons.refresh, size: 18, color: colors.onSurface.withValues(alpha: 0.4)),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
|
|||||||
@@ -63,3 +63,4 @@ flutter:
|
|||||||
- assets/images/
|
- assets/images/
|
||||||
- assets/icon/
|
- assets/icon/
|
||||||
- assets/images/ticket/
|
- assets/images/ticket/
|
||||||
|
- assets/images/imp_book/
|
||||||
|
|||||||
Reference in New Issue
Block a user