Skip to content

Commit 8664175

Browse files
shadowfish07claude
andauthored
feat(bookmark): 为标签编辑功能添加 toast 反馈 (#69)
* feat(bookmark): 为标签编辑功能添加 toast 反馈 - 在 BookmarkCard 中为标签更新操作添加成功/失败的 toast 提示 - 成功时显示"标签已更新"消息 - 失败时显示具体错误信息 - 添加完整的 BookmarkCard 组件测试覆盖 - 测试标签编辑功能和 toast 反馈机制 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> * refactor: 统一使用 SnackBarHelper 替换 ScaffoldMessenger.showSnackBar 重构了整个项目中的 SnackBar 使用方式,将所有直接使用 ScaffoldMessenger.of(context).showSnackBar 的地方替换为使用统一的 SnackBarHelper 辅助类。 主要改动: - 新增 SnackBarHelper 工具类,提供统一的 Material Design 3 风格 SnackBar - 支持成功、错误、信息、警告四种消息类型 - 在以下文件中完成替换: * bookmark_detail_screen.dart: 翻译错误、操作反馈、标签更新等 * daily_read_screen.dart: 加载失败、状态切换错误等 * translation_settings_screen.dart: 设置保存成功/失败反馈 * ai_settings_screen.dart: API 密钥保存失败反馈 * api_config_page.dart: 配置保存失败反馈 * bookmark_list_screen.dart: 标签更新失败反馈 * label_edit_dialog.dart: 标签加载失败反馈 * bookmark_card.dart: 打开链接错误、归档状态、标签更新等 - 更新主题配置,统一 SnackBar 样式 - 所有消息现在都使用统一的视觉风格和行为 这一改动提升了用户体验的一致性,确保所有 SnackBar 都遵循 Material Design 3 规范。 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> * test(ui): 添加书签归档成功提示和SnackBarHelper测试 添加书签归档时的成功提示测试 新增SnackBarHelper测试文件,验证不同状态提示的样式和功能 * fix(bookmark): 修复书签卡片阅读统计数据不显示的问题 - 修改BookmarkRepository._wrapBookmarksWithStats方法,当数据库中没有预计算的阅读统计数据时,自动获取文章内容并计算阅读统计 - 增加ArticleRepository依赖到BookmarkRepository构造函数 - 更新依赖注入配置以提供ArticleRepository - 添加详细的日志记录和错误处理 - 实现懒加载机制:只有在没有统计数据时才进行计算,避免重复计算 - 新增comprehensive测试覆盖,包括自动计算、性能优化和边界情况处理 修复前:书签卡片只显示阅读进度,不显示阅读时间和字数统计 修复后:书签卡片完整显示阅读进度、预计阅读时间和字数统计 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> --------- Co-authored-by: Claude <[email protected]>
1 parent db47e18 commit 8664175

File tree

17 files changed

+1753
-134
lines changed

17 files changed

+1753
-134
lines changed

CLAUDE.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
@.trae/rules/project_rules.md
2+
3+
- TDD编程,开发功能或修复bug时,尽量测试先行

lib/config/dependencies.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ List<SingleChildWidget> providers(String host, String token) {
4545
Provider(create: (context) => ReadingStatsRepository(context.read())),
4646
Provider(
4747
create: (context) =>
48-
BookmarkRepository(context.read(), context.read())),
48+
BookmarkRepository(context.read(), context.read(), context.read())),
4949
Provider(create: (context) => DailyReadHistoryRepository(context.read())),
5050
Provider(create: (context) => LabelRepository(context.read())),
5151
Provider(

lib/data/repository/bookmark/bookmark_repository.dart

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import 'dart:math';
22

3+
import 'package:readeck_app/data/repository/article/article_repository.dart';
34
import 'package:readeck_app/data/repository/reading_stats/reading_stats_repository.dart';
45
import 'package:readeck_app/data/service/readeck_api_client.dart';
56
import 'package:readeck_app/domain/models/bookmark/bookmark.dart';
@@ -11,10 +12,12 @@ import 'package:result_dart/result_dart.dart';
1112
typedef BookmarkChangeListener = void Function();
1213

1314
class BookmarkRepository {
14-
BookmarkRepository(this._readeckApiClient, this._readingStatsRepository);
15+
BookmarkRepository(this._readeckApiClient, this._readingStatsRepository,
16+
this._articleRepository);
1517

1618
final ReadeckApiClient _readeckApiClient;
1719
final ReadingStatsRepository _readingStatsRepository;
20+
final ArticleRepository _articleRepository;
1821

1922
// 全局共享数据管理 - 单一数据源
2023
final List<BookmarkDisplayModel> _bookmarks = [];
@@ -105,7 +108,35 @@ class BookmarkRepository {
105108
) async {
106109
final models = await Future.wait(
107110
bookmarks.map((b) async {
108-
final statsRes = await _readingStatsRepository.getReadingStats(b.id);
111+
// 首先尝试从数据库中读取已有的阅读统计数据
112+
var statsRes = await _readingStatsRepository.getReadingStats(b.id);
113+
114+
// 如果数据库中没有,尝试获取文章内容并计算
115+
if (statsRes.isError()) {
116+
try {
117+
final articleResult =
118+
await _articleRepository.getBookmarkArticle(b.id);
119+
if (articleResult.isSuccess()) {
120+
final htmlContent = articleResult.getOrNull()!;
121+
final calculateResult = await _readingStatsRepository
122+
.calculateAndSaveReadingStats(b.id, htmlContent);
123+
124+
if (calculateResult.isSuccess()) {
125+
statsRes = Success(calculateResult.getOrThrow());
126+
appLogger.d('成功为书签 ${b.id} 计算并保存阅读统计数据');
127+
} else {
128+
appLogger.w(
129+
'计算书签 ${b.id} 的阅读统计数据失败: ${calculateResult.exceptionOrNull()}');
130+
}
131+
} else {
132+
appLogger.w(
133+
'获取书签 ${b.id} 的文章内容失败: ${articleResult.exceptionOrNull()}');
134+
}
135+
} catch (e) {
136+
appLogger.e('处理书签 ${b.id} 的阅读统计数据时发生错误: $e');
137+
}
138+
}
139+
109140
return statsRes.isSuccess()
110141
? BookmarkDisplayModel(bookmark: b, stats: statsRes.getOrThrow())
111142
: BookmarkDisplayModel(bookmark: b);

lib/ui/api_config/widgets/api_config_page.dart

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
22
import 'package:go_router/go_router.dart';
33
import 'package:readeck_app/routing/routes.dart';
44
import 'package:readeck_app/ui/api_config/view_models/api_config_viewmodel.dart';
5+
import 'package:readeck_app/ui/core/ui/snack_bar_helper.dart';
56

67
class ApiConfigPage extends StatefulWidget {
78
const ApiConfigPage({super.key, required this.viewModel});
@@ -93,10 +94,9 @@ class _ApiConfigPageState extends State<ApiConfigPage> {
9394
} catch (e) {
9495
// 弹出错误提示
9596
if (context.mounted) {
96-
ScaffoldMessenger.of(context).showSnackBar(
97-
SnackBar(
98-
content: Text("保存配置失败:$e"),
99-
),
97+
SnackBarHelper.showError(
98+
context,
99+
"保存配置失败:$e",
100100
);
101101
}
102102
}

lib/ui/bookmarks/widget/bookmark_detail_screen.dart

Lines changed: 28 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import 'package:readeck_app/ui/core/ui/bookmark_labels_widget.dart';
1010
import 'package:readeck_app/ui/core/ui/error_page.dart';
1111
import 'package:readeck_app/ui/core/ui/label_edit_dialog.dart';
1212
import 'package:readeck_app/ui/core/ui/loading.dart';
13+
import 'package:readeck_app/ui/core/ui/snack_bar_helper.dart';
1314

1415
class BookmarkDetailScreen extends StatefulWidget {
1516
const BookmarkDetailScreen({super.key, required this.viewModel});
@@ -38,12 +39,9 @@ class _BookmarkDetailScreenState extends State<BookmarkDetailScreen> {
3839
.where((error) => error != null)
3940
.listen((error, subscription) {
4041
if (mounted && error != null) {
41-
ScaffoldMessenger.of(context).showSnackBar(
42-
SnackBar(
43-
content: Text('AI翻译失败: ${error.error.toString()}'),
44-
backgroundColor: Theme.of(context).colorScheme.error,
45-
behavior: SnackBarBehavior.floating,
46-
),
42+
SnackBarHelper.showError(
43+
context,
44+
'AI翻译失败: ${error.error.toString()}',
4745
);
4846
}
4947
});
@@ -75,11 +73,9 @@ class _BookmarkDetailScreenState extends State<BookmarkDetailScreen> {
7573
break;
7674
case 'toggle_mark':
7775
_toggleBookmarkMarked();
78-
ScaffoldMessenger.of(context).showSnackBar(
79-
SnackBar(
80-
content: Text(widget.viewModel.bookmark.isMarked
81-
? '已取消喜爱'
82-
: '已标记喜爱')),
76+
SnackBarHelper.showInfo(
77+
context,
78+
widget.viewModel.bookmark.isMarked ? '已取消喜爱' : '已标记喜爱',
8379
);
8480
break;
8581
case 'edit_labels':
@@ -688,25 +684,19 @@ class _BookmarkDetailScreenState extends State<BookmarkDetailScreen> {
688684
await widget.viewModel.archiveBookmarkCommand.executeWithFuture();
689685

690686
if (mounted) {
691-
ScaffoldMessenger.of(context).showSnackBar(
692-
SnackBar(
693-
content: const Text('已成功归档'),
694-
backgroundColor: Theme.of(context).colorScheme.primary,
695-
behavior: SnackBarBehavior.floating,
696-
),
687+
SnackBarHelper.showSuccess(
688+
context,
689+
'已成功归档',
697690
);
698691

699692
Navigator.of(context).pop();
700693
}
701694
} catch (e) {
702695
// 显示错误提示
703696
if (mounted) {
704-
ScaffoldMessenger.of(context).showSnackBar(
705-
SnackBar(
706-
content: Text('归档失败: ${e.toString()}'),
707-
backgroundColor: Theme.of(context).colorScheme.error,
708-
behavior: SnackBarBehavior.floating,
709-
),
697+
SnackBarHelper.showError(
698+
context,
699+
'归档失败: ${e.toString()}',
710700
);
711701
}
712702
}
@@ -717,8 +707,9 @@ class _BookmarkDetailScreenState extends State<BookmarkDetailScreen> {
717707
await widget.viewModel.toggleMarkCommand.executeWithFuture();
718708
} catch (e) {
719709
if (mounted) {
720-
ScaffoldMessenger.of(context).showSnackBar(
721-
SnackBar(content: Text('操作失败: $e')),
710+
SnackBarHelper.showError(
711+
context,
712+
'操作失败: $e',
722713
);
723714
}
724715
}
@@ -735,14 +726,16 @@ class _BookmarkDetailScreenState extends State<BookmarkDetailScreen> {
735726
try {
736727
await widget.viewModel.updateBookmarkLabels(labels);
737728
if (context.mounted) {
738-
ScaffoldMessenger.of(context).showSnackBar(
739-
const SnackBar(content: Text('标签已更新')),
729+
SnackBarHelper.showSuccess(
730+
context,
731+
'标签已更新',
740732
);
741733
}
742734
} catch (e) {
743735
if (context.mounted) {
744-
ScaffoldMessenger.of(context).showSnackBar(
745-
SnackBar(content: Text('更新标签失败: $e')),
736+
SnackBarHelper.showError(
737+
context,
738+
'更新标签失败: $e',
746739
);
747740
}
748741
}
@@ -787,15 +780,17 @@ class _BookmarkDetailScreenState extends State<BookmarkDetailScreen> {
787780
try {
788781
await widget.viewModel.deleteBookmarkCommand.executeWithFuture();
789782
if (mounted) {
790-
ScaffoldMessenger.of(context).showSnackBar(
791-
const SnackBar(content: Text('书签已删除')),
783+
SnackBarHelper.showSuccess(
784+
context,
785+
'书签已删除',
792786
);
793787
Navigator.of(context).pop(); // 删除成功后返回上一页
794788
}
795789
} catch (e) {
796790
if (mounted) {
797-
ScaffoldMessenger.of(context).showSnackBar(
798-
SnackBar(content: Text('删除失败: $e')),
791+
SnackBarHelper.showError(
792+
context,
793+
'删除失败: $e',
799794
);
800795
}
801796
}

lib/ui/bookmarks/widget/bookmark_list_screen.dart

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import 'package:readeck_app/ui/core/ui/error_page.dart';
99
import 'package:readeck_app/ui/core/ui/loading.dart';
1010
import 'package:readeck_app/ui/bookmarks/view_models/bookmarks_viewmodel.dart';
1111
import 'package:readeck_app/utils/network_error_exception.dart';
12+
import 'package:readeck_app/ui/core/ui/snack_bar_helper.dart';
1213

1314
/// 书签列表页面的文案配置
1415
class BookmarkListTexts {
@@ -225,11 +226,10 @@ class _BookmarkListScreenState<T extends BaseBookmarksViewmodel>
225226
.updateBookmarkLabels(bookmark, labels)
226227
.catchError((error) {
227228
if (context.mounted) {
228-
ScaffoldMessenger.of(context).showSnackBar(
229-
SnackBar(
230-
content: Text('更新标签失败: $error'),
231-
duration: const Duration(seconds: 3),
232-
),
229+
SnackBarHelper.showError(
230+
context,
231+
'更新标签失败: $error',
232+
duration: const Duration(seconds: 3),
233233
);
234234
}
235235
});

lib/ui/core/theme.dart

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,15 @@ abstract final class AppTheme {
3535
borderRadius: BorderRadius.circular(12),
3636
),
3737
),
38+
snackBarTheme: SnackBarThemeData(
39+
behavior: SnackBarBehavior.floating,
40+
backgroundColor: _lightColorScheme.inverseSurface,
41+
contentTextStyle: TextStyle(color: _lightColorScheme.onInverseSurface),
42+
shape: RoundedRectangleBorder(
43+
borderRadius: BorderRadius.circular(12.0),
44+
),
45+
insetPadding: const EdgeInsets.all(16.0),
46+
),
3847
);
3948

4049
static ThemeData darkTheme = ThemeData(
@@ -56,5 +65,14 @@ abstract final class AppTheme {
5665
borderRadius: BorderRadius.circular(12),
5766
),
5867
),
68+
snackBarTheme: SnackBarThemeData(
69+
behavior: SnackBarBehavior.floating,
70+
backgroundColor: _darkColorScheme.inverseSurface,
71+
contentTextStyle: TextStyle(color: _darkColorScheme.onInverseSurface),
72+
shape: RoundedRectangleBorder(
73+
borderRadius: BorderRadius.circular(12.0),
74+
),
75+
insetPadding: const EdgeInsets.all(16.0),
76+
),
5977
);
6078
}

lib/ui/core/ui/bookmark_card.dart

Lines changed: 30 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import 'package:flutter_command/flutter_command.dart';
44
import 'package:readeck_app/domain/models/bookmark/bookmark.dart';
55
import 'package:readeck_app/ui/core/ui/bookmark_labels_widget.dart';
66
import 'package:readeck_app/ui/core/ui/label_edit_dialog.dart';
7+
import 'package:readeck_app/ui/core/ui/snack_bar_helper.dart';
78
import 'package:readeck_app/utils/reading_stats_calculator.dart';
89

910
class BookmarkCard extends StatefulWidget {
@@ -38,26 +39,17 @@ class _BookmarkCardState extends State<BookmarkCard> {
3839
@override
3940
didChangeDependencies() {
4041
widget.onOpenUrl.errors.where((x) => x != null).listen((error, _) {
41-
ScaffoldMessenger.of(context).showSnackBar(
42-
SnackBar(
43-
content: Text(error.toString()),
44-
backgroundColor: Theme.of(context).colorScheme.error,
45-
duration: const Duration(seconds: 5),
46-
action: SnackBarAction(
47-
label: '复制链接',
48-
textColor: Theme.of(context).colorScheme.onError,
49-
onPressed: () async {
50-
await Clipboard.setData(ClipboardData(text: widget.bookmark.url));
51-
if (mounted) {
52-
ScaffoldMessenger.of(context).showSnackBar(
53-
const SnackBar(
54-
content: Text('链接已复制到剪贴板'),
55-
duration: Duration(seconds: 2),
56-
),
57-
);
58-
}
59-
},
60-
),
42+
SnackBarHelper.showError(
43+
context,
44+
error.toString(),
45+
action: SnackBarAction(
46+
label: '复制链接',
47+
onPressed: () async {
48+
await Clipboard.setData(ClipboardData(text: widget.bookmark.url));
49+
if (mounted) {
50+
SnackBarHelper.showSuccess(context, '链接已复制到剪贴板');
51+
}
52+
},
6153
),
6254
);
6355
});
@@ -279,13 +271,10 @@ class _BookmarkCardState extends State<BookmarkCard> {
279271
onPressed: widget.onToggleArchive != null
280272
? () {
281273
widget.onToggleArchive!(widget.bookmark);
282-
ScaffoldMessenger.of(context).showSnackBar(
283-
SnackBar(
284-
content: Text(widget.bookmark.isArchived
285-
? '已取消归档'
286-
: '已标记归档'),
287-
duration: const Duration(seconds: 2),
288-
),
274+
SnackBarHelper.showSuccess(
275+
context,
276+
widget.bookmark.isArchived ? '已取消归档' : '已标记归档',
277+
duration: const Duration(seconds: 2),
289278
);
290279
}
291280
: null,
@@ -351,7 +340,20 @@ class _BookmarkCardState extends State<BookmarkCard> {
351340
builder: (dialogContext) => LabelEditDialog(
352341
bookmark: widget.bookmark,
353342
availableLabels: labels,
354-
onUpdateLabels: widget.onUpdateLabels!,
343+
onUpdateLabels: (bookmark, labels) async {
344+
try {
345+
if (widget.onUpdateLabels != null) {
346+
widget.onUpdateLabels!(bookmark, labels);
347+
if (context.mounted) {
348+
SnackBarHelper.showSuccess(context, '标签已更新');
349+
}
350+
}
351+
} catch (e) {
352+
if (context.mounted) {
353+
SnackBarHelper.showError(context, '更新标签失败: $e');
354+
}
355+
}
356+
},
355357
onLoadLabels: widget.onLoadLabels,
356358
),
357359
);

lib/ui/core/ui/label_edit_dialog.dart

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import 'package:flutter/material.dart';
22
import 'package:readeck_app/domain/models/bookmark/bookmark.dart';
3+
import 'package:readeck_app/ui/core/ui/snack_bar_helper.dart';
34

45
class LabelEditDialog extends StatefulWidget {
56
final Bookmark bookmark;
@@ -69,11 +70,9 @@ class _LabelEditDialogState extends State<LabelEditDialog> {
6970
_isLoading = false;
7071
}
7172
});
72-
ScaffoldMessenger.of(context).showSnackBar(
73-
SnackBar(
74-
content: Text('加载标签失败: $e'),
75-
backgroundColor: Theme.of(context).colorScheme.error,
76-
),
73+
SnackBarHelper.showError(
74+
context,
75+
'加载标签失败: $e',
7776
);
7877
}
7978
}

0 commit comments

Comments
 (0)