Skip to content

Conversation

@shadowfish07
Copy link
Owner

@shadowfish07 shadowfish07 commented Aug 16, 2025

Summary

close #64,为标签编辑功能添加用户反馈机制:

  • 在 BookmarkCard 组件中为标签更新操作添加 toast 反馈
  • 成功更新时显示"标签已更新"消息
  • 失败时显示具体错误信息"更新标签失败: $error"
  • 使用 context.mounted 检查确保安全的 toast 显示

Changes

主要修改

  • lib/ui/core/ui/bookmark_card.dart: 修改 _showLabelEditDialog 方法,包装 onUpdateLabels 回调以添加 toast 反馈

测试覆盖

  • test/ui/core/ui/bookmark_card_test.dart: 新增完整的 BookmarkCard 组件测试
    • 测试标签编辑按钮的显示和交互
    • 测试成功更新时的 toast 反馈
    • 测试失败时的错误 toast 反馈
    • 测试基本组件功能

Test plan

  • 运行 flutter analyze - 无警告无错误
  • 运行新添加的测试 - 全部通过 (10 个测试用例)
  • 验证与现有代码的兼容性
  • 确保遵循项目架构规范和编码风格

Screenshots

功能演示:

  • 成功更新标签时会显示绿色 toast:"标签已更新"
  • 更新失败时会显示红色 toast:"更新标签失败: [具体错误]"

🤖 Generated with Claude Code

Summary by CodeRabbit

  • 新功能
    • 引入统一提示条组件,支持成功/错误/信息/警告提示;打开链接失败时可一键复制链接;标签更新等操作提供明确反馈。
  • 体验优化/Style
    • 全局提示条采用统一的 MD3 浮动样式与主题配色,主题新增提示条样式,交互更一致。
  • Bug 修复
    • 书签缺失阅读统计时自动补全计算,保证列表与详情展示完整。
  • 文档
    • 新增协作规范说明条目。
  • 测试
    • 增加提示条、书签卡片与数据仓库的单元/组件测试,覆盖成功与异常场景。

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 16, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

此更改集中引入统一的 SnackBarHelper 并在多处 UI 中替换原有 ScaffoldMessenger 调用;为主题加入全局 SnackBar 主题;扩展 BookmarkRepository 以在缺失阅读统计时自动获取文章并计算保存统计;更新依赖注入;新增对应单元与组件测试;补充文档。

Changes

Cohort / File(s) Summary
统一提示组件:SnackBarHelper 引入与替换
lib/ui/core/ui/snack_bar_helper.dart, lib/ui/core/ui/label_edit_dialog.dart, lib/ui/bookmarks/widget/bookmark_detail_screen.dart, lib/ui/bookmarks/widget/bookmark_list_screen.dart, lib/ui/daily_read/widgets/daily_read_screen.dart, lib/ui/settings/widgets/ai_settings_screen.dart, lib/ui/settings/widgets/translation_settings_screen.dart, lib/ui/core/ui/bookmark_card.dart, test/ui/core/ui/snack_bar_helper_test.dart
新增 SnackBarHelper,集中封装成功/错误/信息/警告提示;多处 UI 从直接使用 ScaffoldMessenger 改为调用 SnackBarHelper;新增对应组件测试覆盖各方法与主题适配。
主题适配
lib/ui/core/theme.dart
为浅色与深色主题添加 snackBarTheme(浮动、圆角、间距、颜色源自配色方案)。
书签仓库扩展与依赖注入
lib/data/repository/bookmark/bookmark_repository.dart, lib/config/dependencies.dart
BookmarkRepository 新增 ArticleRepository 依赖;当阅读统计缺失/错误时尝试获取文章并计算保存统计;更新构造与依赖注入为三参。
仓库单元测试与 Mock
test/unit/data/repository/bookmark/bookmark_repository_test.dart, test/unit/data/repository/bookmark/bookmark_repository_test.mocks.dart
新增针对缺失统计自动计算、多场景分支及调用顺序的单元测试;引入 Mockito 生成的 ReadeckApiClient/ReadingStatsRepository/ArticleRepository 的 Mock。
组件测试:BookmarkCard
test/ui/core/ui/bookmark_card_test.dart
新增 BookmarkCard 测试:标签编辑入口与对话框、成功/失败提示、归档按钮行为、现有标签展示与渲染检查。
文档
CLAUDE.md
增加一行引用与一条中文要点说明,无功能变化。

Sequence Diagram(s)

sequenceDiagram
  participant UI as UI/调用方
  participant Repo as BookmarkRepository
  participant Stats as ReadingStatsRepository
  participant Article as ArticleRepository

  UI->>Repo: 加载书签列表
  loop 对每个书签
    Repo->>Stats: getReadingStats(bookmarkId)
    alt 有统计
      Repo-->>UI: 返回含统计的 BookmarkDisplayModel
    else 缺失/错误
      Repo->>Article: getBookmarkArticle(bookmarkId)
      alt 获取成功
        Repo->>Stats: calculateAndSaveReadingStats(article)
        alt 计算成功
          Repo-->>UI: 返回含新统计的 BookmarkDisplayModel
        else 计算失败
          Repo-->>UI: 返回不含统计的 BookmarkDisplayModel(记录日志)
        end
      else 获取失败
        Repo-->>UI: 返回不含统计的 BookmarkDisplayModel(记录日志)
      end
    end
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~30 minutes

Assessment against linked issues

Objective Addressed Explanation
添加标签后增加反馈(#64

Assessment against linked issues: Out-of-scope changes

Code Change Explanation
扩展 BookmarkRepository:引入 ArticleRepository、缺失统计时拉取文章并计算保存(lib/data/repository/bookmark/bookmark_repository.dart,多个位置) 与“添加标签后增加反馈”无直接关联,属数据层行为变更。
更新依赖注入为 BookmarkRepository 增加第三个依赖(lib/config/dependencies.dart,多个位置) 仅为支持仓库新逻辑,非标签反馈范围。
新增全局 SnackBar 主题(lib/ui/core/theme.dart,多个位置) 全局样式配置,超出仅“标签添加反馈”的具体目标。
新增 Snackbar 助手并在多处页面替换通用错误/信息提示(lib/ui/core/ui/snack_bar_helper.dart 及多处 UI 文件) 普适性提示重构,不限于“标签添加后”的反馈。
新增 BookmarkRepository 单元测试与 Mock(test/unit/data/repository/bookmark/*,多个位置) 覆盖数据层统计计算逻辑,非标签反馈目标。

Possibly related PRs

Suggested labels

enhancement

Poem

小兔挥耳叮铃铃,
一按“保存”响轻鸣;
成功绿光蹦又跳,
失败红云也温馨。
标签更新咚咚响,
提示飘浮夜色明。
啊——SnackBar,胡萝卜般甜盈。

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/label-edit-toast-feedback

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary of Changes

Hello @shadowfish07, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

此拉取请求主要为书签标签编辑功能引入了用户反馈机制,通过 Toast 消息清晰地告知用户标签更新操作的结果。同时,它还修复了每日阅读页面加载指示器的显示逻辑,并为 BookmarkCard 组件和相关 ViewModel 增加了全面的单元测试,提升了应用的稳定性和用户体验。

Highlights

  • 书签标签编辑功能添加 Toast 反馈: 在 BookmarkCard 组件中,为标签更新操作添加了用户反馈机制。成功时会显示“标签已更新”的绿色提示,失败时则会显示“更新标签失败: [具体错误]”的红色提示,提升了用户体验。
  • 引入 context.mounted 安全检查: 在显示 Toast 消息之前,新增了 context.mounted 检查。这确保了当 Widget 不再挂载在 Widget 树上时,不会尝试显示 Toast,从而避免了潜在的运行时错误和内存泄漏。
  • 新增 BookmarkCard 组件测试: 为 BookmarkCard 组件新增了全面的单元测试,覆盖了标签编辑按钮的显示、对话框交互,以及成功和失败场景下的 Toast 反馈,显著提高了代码的健壮性和可靠性。
  • 优化 DailyReadScreen 加载指示器显示逻辑: 修复了 DailyReadScreen 中加载指示器的显示逻辑,现在只有在 lastValue 不为空且为空列表时才显示“正在加载今日推荐”提示,避免了在已有数据时重复显示加载状态,优化了用户界面。
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point in your pull request via creating an issue comment (i.e. comment on the pull request page) using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in issue comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

这个 PR 的目标是为标签编辑功能添加 toast 反馈,这是一个很好的用户体验改进。实现的核心逻辑在 BookmarkCard 中,通过 try-catch 来处理成功和失败的场景。同时,PR 还添加了全面的单元测试来覆盖新功能,值得称赞。

主要的反馈集中在 BookmarkCard 中对异步操作的处理上。当前的实现没有 await 标签更新的异步回调,导致成功/失败的反馈逻辑不正确。我提供了一个修复建议,但这可能需要对相关回调的函数签名进行一些连锁修改。除此之外,其他文件的修改和新增的测试看起来都很不错。

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (7)
test/ui/daily_read/view_models/daily_read_viewmodel_test.dart (1)

99-101: 用 untilCalled 精准等待异步调用,替代零延时等待

当前用 Future.delayed(Duration.zero) 依赖微任务调度,存在脆弱性。建议等待具体的 mock 调用发生,确保 ViewModel 的首轮加载逻辑已推进到关键点。

应用如下修改提升稳定性:

-      // The load command is executed in the constructor. We need to wait for it to complete.
-      await Future.delayed(Duration.zero);
+      // 等待首个用例关键调用发生,避免依赖微任务时序
+      await untilCalled(
+        mockDailyReadHistoryRepository.getTodayDailyReadHistory(),
+      );
test/ui/daily_read/widgets/daily_read_screen_test.dart (3)

42-49: Provider 选择合理,但可考虑最小化约束以降低耦合

当前使用 ChangeNotifierProvider.value 搭配 MockDailyReadViewModel(实现了 ChangeNotifier 的接口方法)是可行的;若后续 Mock 不再需要触发监听或你希望规避 ChangeNotifier 语义约束,亦可改用 Provider.value。无须修改,供参考。


96-129: 加载态用例建议增强稳定性与断言力度

当前通过手动 execute 和短暂 pump(10ms) 等待,存在轻微的时间片依赖与偶发不稳。建议:

  • 明确断言命令确在执行中(isExecuting)。
  • 放宽等待窗口,避免 10ms 导致的偶发错过。

可在该用例内做如下微调:

-      // Wait for the widget to be built and command to start executing
-      await tester.pump(const Duration(milliseconds: 10));
+      // Wait for the widget to be built and ensure command is executing
+      await tester.pump(const Duration(milliseconds: 20));
+      expect(loadCommand.isExecuting, isTrue);
 
       // Assert - should show loading when command is executing with empty initial value
       expect(find.text('正在加载今日推荐'), findsOneWidget);
 
-      // Wait for the command to complete to avoid pending timer issues
-      await tester.pumpAndSettle();
+      // Wait for the command to complete to avoid pending timer issues
+      await tester.pumpAndSettle();

51-130: 补上一些边界态的 UI 测试(可选)

建议再加两类用例以覆盖主干路径:

  • 空数据完成加载后展示“空态”文案/占位(若有)。
  • load 失败时的错误提示/重试入口(若有)。

我可以按现有模式补充用例,是否需要我起草测试代码?

test/ui/core/ui/bookmark_card_test.dart (3)

80-101: 成功流程可再断言“对话框已关闭”,并避免对具体实现类型的过度耦合

目前已断言成功 toast 文案与 SnackBar 存在。为强化 UX 预期,建议补充断言保存后对话框关闭。同时,若后续从 SnackBar 切换为 Overlay toast,此用例会因类型断言而破;可以仅保留文案断言或改用 Key 断言,提升鲁棒性。

       // Assert - check that success toast is shown
       expect(find.text('标签已更新'), findsOneWidget);
-      expect(find.byType(SnackBar), findsOneWidget);
+      expect(find.byType(SnackBar), findsOneWidget); // 如后续改用 Overlay,这行可移除
+
+      // 对话框应已关闭
+      expect(find.byType(LabelEditDialog), findsNothing);
 
       // Verify the callback was called
       expect(updateLabelsCalled, isTrue);

103-127: 错误流程建议同时断言错误细节透传

当前仅断言包含“更新标签失败”,可再断言异常详情(Network error)被拼接到 toast 中,匹配生产逻辑“更新标签失败: $error”。

       // Assert - check that error toast is shown
       expect(find.textContaining('更新标签失败'), findsOneWidget);
+      expect(find.textContaining('Network error'), findsOneWidget);
       expect(find.byType(SnackBar), findsOneWidget);

129-137: 已有标签展示断言很好,建议补充 onLoadLabels 分支的覆盖(可选)

当前依赖 availableLabels。可再加一例:仅提供 onLoadLabels(返回异步标签),不传 availableLabels,验证打开对话框时异步拉取并展示。

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled
  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between db47e18 and 5b90ab5.

📒 Files selected for processing (8)
  • CLAUDE.md (1 hunks)
  • lib/ui/core/ui/bookmark_card.dart (1 hunks)
  • lib/ui/daily_read/widgets/daily_read_screen.dart (1 hunks)
  • test/ui/core/ui/bookmark_card_test.dart (1 hunks)
  • test/ui/daily_read/view_models/daily_read_viewmodel_test.dart (1 hunks)
  • test/ui/daily_read/view_models/daily_read_viewmodel_test.mocks.dart (1 hunks)
  • test/ui/daily_read/widgets/daily_read_screen_test.dart (1 hunks)
  • test/ui/daily_read/widgets/daily_read_screen_test.mocks.dart (1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
lib/**/*.dart

📄 CodeRabbit Inference Engine (.trae/rules/project_rules.md)

所有样式必须通过主题提供,不允许硬编码颜色、字号等(例如禁止直接使用 Color/0xFF/固定 fontSize)

Files:

  • lib/ui/daily_read/widgets/daily_read_screen.dart
  • lib/ui/core/ui/bookmark_card.dart
lib/ui/**/*[Ss]creen.dart

📄 CodeRabbit Inference Engine (.trae/rules/project_rules.md)

lib/ui/**/*[Ss]creen.dart: View(Screen)构造函数仅接受 key 与 viewModel;不包含任何业务逻辑;用户交互通过 Command 执行;处理 Command 的执行中/错误/完成状态
在 StatefulWidget 中通过 Command 监听器处理完成与错误:initState 中订阅 results/errors,dispose 中取消,使用 mounted 检查
所有错误状态应统一使用 ErrorPage 组件,优先通过工厂方法(如 ErrorPage.fromException)创建并遵循主题

Files:

  • lib/ui/daily_read/widgets/daily_read_screen.dart
🧠 Learnings (7)
📚 Learning: 2025-08-13T18:04:36.250Z
Learnt from: CR
PR: shadowfish07/ReadeckAPP#0
File: .trae/rules/project_rules.md:0-0
Timestamp: 2025-08-13T18:04:36.250Z
Learning: Applies to test/widget/**/*.dart : 为关键 Widget 提供 UI 测试

Applied to files:

  • test/ui/daily_read/widgets/daily_read_screen_test.dart
  • test/ui/core/ui/bookmark_card_test.dart
  • test/ui/daily_read/view_models/daily_read_viewmodel_test.dart
📚 Learning: 2025-08-13T18:04:36.250Z
Learnt from: CR
PR: shadowfish07/ReadeckAPP#0
File: .trae/rules/project_rules.md:0-0
Timestamp: 2025-08-13T18:04:36.250Z
Learning: Applies to test/unit/ui/**/*.dart : ViewModel 层需具备 100% 单元测试覆盖,并包含 Result 处理逻辑测试

Applied to files:

  • test/ui/daily_read/widgets/daily_read_screen_test.dart
  • test/ui/core/ui/bookmark_card_test.dart
  • test/ui/daily_read/view_models/daily_read_viewmodel_test.dart
📚 Learning: 2025-08-13T18:04:36.250Z
Learnt from: CR
PR: shadowfish07/ReadeckAPP#0
File: .trae/rules/project_rules.md:0-0
Timestamp: 2025-08-13T18:04:36.250Z
Learning: Applies to lib/**/*ViewModel.dart : ViewModel 必须继承 ChangeNotifier

Applied to files:

  • test/ui/daily_read/widgets/daily_read_screen_test.dart
📚 Learning: 2025-08-13T18:04:36.250Z
Learnt from: CR
PR: shadowfish07/ReadeckAPP#0
File: .trae/rules/project_rules.md:0-0
Timestamp: 2025-08-13T18:04:36.250Z
Learning: Applies to test/integration/**/*.dart : 编写集成测试覆盖核心用户流程与 Command 执行链路

Applied to files:

  • test/ui/daily_read/widgets/daily_read_screen_test.dart
  • test/ui/core/ui/bookmark_card_test.dart
📚 Learning: 2025-08-13T18:04:36.250Z
Learnt from: CR
PR: shadowfish07/ReadeckAPP#0
File: .trae/rules/project_rules.md:0-0
Timestamp: 2025-08-13T18:04:36.250Z
Learning: Applies to test/unit/data/**/*.dart : Repository 层需具备 100% 单元测试覆盖

Applied to files:

  • test/ui/daily_read/view_models/daily_read_viewmodel_test.dart
📚 Learning: 2025-08-13T18:04:36.250Z
Learnt from: CR
PR: shadowfish07/ReadeckAPP#0
File: .trae/rules/project_rules.md:0-0
Timestamp: 2025-08-13T18:04:36.250Z
Learning: Applies to lib/**/*ViewModel.dart : 对外暴露的异步操作通过 flutter_command 的 Command 暴露;UI 状态采用 private setter + public getter

Applied to files:

  • lib/ui/daily_read/widgets/daily_read_screen.dart
📚 Learning: 2025-08-13T18:04:36.250Z
Learnt from: CR
PR: shadowfish07/ReadeckAPP#0
File: .trae/rules/project_rules.md:0-0
Timestamp: 2025-08-13T18:04:36.250Z
Learning: Applies to lib/**/*ViewModel.dart : ViewModel 通过私有 final 成员持有 Repository 引用(不直接依赖 Service),并在构造函数中初始化所有 Commands

Applied to files:

  • lib/ui/daily_read/widgets/daily_read_screen.dart
🔇 Additional comments (7)
CLAUDE.md (1)

1-1: 文档引用变更符合预期

仅添加了项目规则引用,未影响功能代码。无其他建议。

test/ui/daily_read/view_models/daily_read_viewmodel_test.mocks.dart (1)

1-554: 自动生成的 Mockito Mocks 文件,无需手动修改

文件由 Mockito 5.4.6 生成,覆盖仓库内相关依赖接口,默认桩实现合理。建议保持由代码生成器维护,勿手改。

test/ui/daily_read/view_models/daily_read_viewmodel_test.dart (1)

32-43: 为 ResultDart 注册 dummy 值的做法妥当

提前通过 provideDummy 配置常用 Result 类型,能避免 Mockito 抛出缺省桩异常,测试更稳定。此段实现 LGTM。

test/ui/daily_read/widgets/daily_read_screen_test.dart (1)

21-40: 测试桩搭建到位,覆盖了构建期可能触达的命令

为 openUrl/toggleArchived/toggleMarked/loadLabels 等命令提供真实 Command stub,降低了 Mockito 行为桩带来的异步不确定性,利于稳定性。

test/ui/core/ui/bookmark_card_test.dart (2)

33-51: 测试装配清晰,便于覆盖多分支行为

以真实 Command + 可注入回调的方式构建被测 Widget,便于模拟成功/失败路径,结构清晰。


170-178: 进度展示用例 LGTM

同时验证了文本与进度指示器的存在,覆盖了视觉与可访问性关键信息。

test/ui/daily_read/widgets/daily_read_screen_test.mocks.dart (1)

1-250: 生成的 Mockito 桩文件无需手改,当前与用例契合

覆盖了 DailyReadViewModel 所需的命令、属性与监听 API,满足测试注入与断言需求。

shadowfish07 and others added 4 commits August 16, 2025 11:11
- 在 BookmarkCard 中为标签更新操作添加成功/失败的 toast 提示
- 成功时显示"标签已更新"消息
- 失败时显示具体错误信息
- 添加完整的 BookmarkCard 组件测试覆盖
- 测试标签编辑功能和 toast 反馈机制

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
重构了整个项目中的 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 <noreply@anthropic.com>
添加书签归档时的成功提示测试
新增SnackBarHelper测试文件,验证不同状态提示的样式和功能
- 修改BookmarkRepository._wrapBookmarksWithStats方法,当数据库中没有预计算的阅读统计数据时,自动获取文章内容并计算阅读统计
- 增加ArticleRepository依赖到BookmarkRepository构造函数
- 更新依赖注入配置以提供ArticleRepository
- 添加详细的日志记录和错误处理
- 实现懒加载机制:只有在没有统计数据时才进行计算,避免重复计算
- 新增comprehensive测试覆盖,包括自动计算、性能优化和边界情况处理

修复前:书签卡片只显示阅读进度,不显示阅读时间和字数统计
修复后:书签卡片完整显示阅读进度、预计阅读时间和字数统计

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🔭 Outside diff range comments (2)
lib/ui/bookmarks/widget/bookmark_detail_screen.dart (1)

723-740: 在 Dialog builder 的 context 中显示 SnackBar,关闭弹窗后 context 会失效,导致提示不显示

回调中使用的是 builder 提供的 context,而 LabelEditDialog 的保存逻辑会先 Navigator.pop() 关闭 Dialog。等到 await updateBookmarkLabels 完成时,builder 的 context 往往已 unmounted,因此 context.mounted 为 false,提示被跳过,用户看不到“标签已更新/失败”。

建议改为使用页面级 context 展示提示,可按如下方式修正:

-  void _showLabelEditDialog() {
-    showDialog<void>(
-      context: context,
-      builder: (BuildContext context) {
+  void _showLabelEditDialog() {
+    // 捕获页面级 context,避免 Dialog 关闭后 context 失效
+    final rootContext = context;
+    showDialog<void>(
+      context: rootContext,
+      builder: (BuildContext dialogContext) {
         return LabelEditDialog(
           bookmark: widget.viewModel.bookmark,
           availableLabels: widget.viewModel.availableLabels,
           onUpdateLabels: (bookmark, labels) async {
             try {
               await widget.viewModel.updateBookmarkLabels(labels);
-              if (context.mounted) {
-                SnackBarHelper.showSuccess(
-                  context,
+              if (mounted) {
+                SnackBarHelper.showSuccess(
+                  rootContext,
                   '标签已更新',
                 );
               }
             } catch (e) {
-              if (context.mounted) {
-                SnackBarHelper.showError(
-                  context,
+              if (mounted) {
+                SnackBarHelper.showError(
+                  rootContext,
                   '更新标签失败: $e',
                 );
               }
             }
           },
           onLoadLabels: () => widget.viewModel.loadLabels.executeWithFuture(),
         );
       },
     );
   }

如需更彻底与通用,可改为 Dialog 返回所选标签,外层 await showDialog 后再更新并提示。

lib/ui/daily_read/widgets/daily_read_screen.dart (1)

53-86: 在 didChangeDependencies 中多次注册错误监听且未取消,存在内存泄漏与重复提示风险

每次依赖变化都会调用 listen,但没有保存并在 dispose 中取消订阅;久而久之会出现重复 SnackBar 与资源泄漏。此外,监听回调中直接使用 context,未做 mounted 判定,页面销毁后可能抛错。

建议:

  • 将订阅移动到 initState,用 ListenableSubscription 字段保存;
  • dispose 中统一 cancel()
  • 回调中加 if (!mounted) return;
  • 或者完全依赖 CommandBuilder 的 onError 渲染 ErrorPage,减少 SnackBar 冗余。

可参考如下改动(示意,需在类中新增字段并调整 import 已满足类型):

// 类字段:
late ListenableSubscription _loadErrSub;
late ListenableSubscription _toggleArchiveErrSub;
late ListenableSubscription _toggleMarkErrSub;

@override
void initState() {
  super.initState();
  _confettiController = ConfettiController(duration: const Duration(seconds: 3));
  widget.viewModel.setOnBookmarkArchivedCallback(_onBookmarkArchived);

  _loadErrSub = widget.viewModel.load.errors.where((x) => x != null).listen((error, _) {
    if (!mounted) return;
    appLogger.e('加载书签失败', error: error);
    SnackBarHelper.showError(context, '加载书签失败');
  });
  _toggleArchiveErrSub = widget.viewModel.toggleBookmarkArchived.errors.where((x) => x != null).listen((error, _) {
    if (!mounted) return;
    appLogger.e('切换书签归档状态失败', error: error);
    SnackBarHelper.showError(context, '切换书签归档状态失败');
  });
  _toggleMarkErrSub = widget.viewModel.toggleBookmarkMarked.errors.where((x) => x != null).listen((error, _) {
    if (!mounted) return;
    appLogger.e('切换书签标记状态失败', error: error);
    SnackBarHelper.showError(context, '切换书签标记状态失败');
  });
}

@override
void dispose() {
  _confettiController.dispose();
  widget.viewModel.setOnBookmarkArchivedCallback(null);
  _loadErrSub.cancel();
  _toggleArchiveErrSub.cancel();
  _toggleMarkErrSub.cancel();
  super.dispose();
}

如你更倾向保留 didChangeDependencies,也需保证只订阅一次并在 dispose 取消。

♻️ Duplicate comments (2)
lib/ui/daily_read/widgets/daily_read_screen.dart (1)

229-231: whileExecuting 的 Loading 判定会错过“首次加载”场景(沿用先前建议)

建议在首次加载(lastValue == null)或用户主动刷新(param == true)时显示 Loading,以改善首屏体验。

-            if (lastValue != null && lastValue.isEmpty) {
+            // 首次加载(lastValue == null)或用户主动刷新(param == true)时展示 Loading
+            if (lastValue == null || param == true) {
               return const Loading(text: '正在加载今日推荐');
             }
lib/ui/core/ui/bookmark_card.dart (1)

343-356: 修复:未等待异步 onUpdateLabels 导致“先成功后失败”双重提示

此处直接调用 widget.onUpdateLabels!await。若调用方返回 Future 且最终失败,会出现“先显示成功,再显示失败”的冲突体验;同时 try/catch 无法捕获异步抛出的异常。这与先前审查意见一致。

建议兼容同步/异步两种回调:动态调用并在返回为 Future 时等待完成,再决定是否展示成功提示。同时避免与外层同名变量混淆,重命名参数为 selectedLabels

-          onUpdateLabels: (bookmark, labels) async {
+          onUpdateLabels: (bookmark, selectedLabels) async {
             try {
               if (widget.onUpdateLabels != null) {
-                widget.onUpdateLabels!(bookmark, labels);
-                if (context.mounted) {
-                  SnackBarHelper.showSuccess(context, '标签已更新');
-                }
+                final ret = widget.onUpdateLabels!(bookmark, selectedLabels);
+                if (ret is Future) {
+                  await ret;
+                }
+                if (context.mounted) {
+                  SnackBarHelper.showSuccess(context, '标签已更新');
+                }
               }
             } catch (e) {
               if (context.mounted) {
                 SnackBarHelper.showError(context, '更新标签失败: $e');
               }
             }
           },

补充建议(可选但更清晰):

  • 将回调签名升级为 FutureOr<void> Function(Bookmark, List<String>)?,从类型层面明确“可异步”:
// 需要:import 'dart:async';
final FutureOr<void> Function(Bookmark bookmark, List<String> labels)? onUpdateLabels;

这样 IDE 与调用方都能得到更明确的约束与提示,减少误用。

🧹 Nitpick comments (12)
lib/config/dependencies.dart (1)

48-48: 为 BookmarkRepository 的依赖注入显式标注泛型,提升可读性与类型安全

当前使用 context.read() 依赖下行类型推断,虽然大多情况下可工作,但在重构或参数顺序调整时容易埋坑。建议显式标注泛型,代码更直观可维护。

可在该行应用如下变更:

-            BookmarkRepository(context.read(), context.read(), context.read())),
+            BookmarkRepository(
+              context.read<ReadeckApiClient>(),
+              context.read<ReadingStatsRepository>(),
+              context.read<ArticleRepository>(),
+            )),

(额外建议:文件内其它 Provider 同样存在 context.read() 未显式泛型的写法,如 ArticleRepository 与 BookmarkOperationUseCases 的注入,可一并调整以保持一致性。)

lib/data/repository/bookmark/bookmark_repository.dart (1)

111-139: 将“未找到统计数据”与“读取统计数据出错”区分处理,避免误触发重算

当前以 statsRes.isError() 为条件即进入“补抓文章 + 计算统计”的分支,会将所有失败(如数据库暂时性故障)都当作“无统计数据”处理,可能导致重复抓取与重算,增加负载并掩盖异常。

建议:

  • 为 ReadingStatsRepository.getReadingStats 的失败类型细分(如定义/沿用 NotFound 异常或错误码),仅在明确“未找到”时才触发补抓与重算;其它异常直接日志并返回空 stats。
  • 记录重算的计量日志/metrics,便于观察是否出现异常重算峰值。
test/unit/data/repository/bookmark/bookmark_repository_test.dart (1)

1-587: 测试用例覆盖全面,验证路径与交互顺序清晰可依

  • 覆盖了已有统计、缺失统计(补抓文章再计算保存)、文章抓取失败、统计计算失败、异常抛出、多书签混合场景等关键路径,且通过 verify/verifyInOrder 校验交互顺序,质量不错。
  • setUpAll 中对 appLogger 初始化与 provideDummy 处理得当,避免 Mockito 运行期报错。
  • 每组 API 列表加载(未归档/已归档/已标记)均验证了统计获取/计算逻辑,契合生产代码改动点。

可选补充:

  • 适度补充对 toggleMarked/toggleArchived/updateLabels/updateReadProgress/deleteBookmark 的单测,确保 Repository 层整体行为长期保持高覆盖与回归安全。
lib/ui/core/theme.dart (1)

38-46: 建议让 SnackBarHelper 遵循主题,去除重复的 margin/shape/behavior 硬编码

AppTheme 已设置 snackBarTheme,但 SnackBarHelper 里仍显式设置了 floating/margin/shape,导致配置分散且可能不一致。建议在 Helper 中仅负责语义色彩与时长,布局外观完全交给 Theme(尤其是 insetPadding vs 实例级 margin 的选择应统一)。

Also applies to: 68-76

lib/ui/core/ui/snack_bar_helper.dart (3)

82-99: 去除行为/边距/圆角的硬编码,统一交由 SnackBarTheme 管控

App 主题已定义 snackBarTheme,这里重复设置了 behavior/margin/shape。建议删除这些实例级样式,避免后续主题调整与 Helper 写死值不一致。

[建议变更示例]

     ScaffoldMessenger.of(context).showSnackBar(
       SnackBar(
         content: Text(
           message,
           style: TextStyle(color: textColor),
         ),
         backgroundColor: backgroundColor,
-        behavior: SnackBarBehavior.floating,
-        margin: const EdgeInsets.all(16.0),
-        shape: RoundedRectangleBorder(
-          borderRadius: BorderRadius.circular(12.0),
-        ),
         duration: duration,
         action: action?.copyWith(
           textColor: textColor,
         ),
       ),
     );

82-83: 避免 SnackBar 堆叠:显示前先隐藏当前 SnackBar

快速连续触发时可出现堆叠或队列滞留。显示前先 hideCurrentSnackBar 有助于更贴近“最新状态”反馈。

-    ScaffoldMessenger.of(context).showSnackBar(
+    final messenger = ScaffoldMessenger.of(context);
+    messenger.hideCurrentSnackBar();
+    messenger.showSnackBar(

103-115: SnackBarAction.copyWith 可拓展以提升通用性(可选)

当前仅支持 textColor 复制,若进一步支持 label/onPressed/disabledTextColor,会更贴合“copyWith”的预期用法(不影响现有调用)。

-extension SnackBarActionCopyWith on SnackBarAction {
-  SnackBarAction copyWith({
-    Color? textColor,
-  }) {
-    return SnackBarAction(
-      label: label,
-      onPressed: onPressed,
-      textColor: textColor ?? this.textColor,
-      disabledTextColor: disabledTextColor,
-    );
-  }
-}
+extension SnackBarActionCopyWith on SnackBarAction {
+  SnackBarAction copyWith({
+    String? label,
+    VoidCallback? onPressed,
+    Color? textColor,
+    Color? disabledTextColor,
+  }) {
+    return SnackBarAction(
+      label: label ?? this.label,
+      onPressed: onPressed ?? this.onPressed,
+      textColor: textColor ?? this.textColor,
+      disabledTextColor: disabledTextColor ?? this.disabledTextColor,
+    );
+  }
+}
lib/ui/settings/widgets/translation_settings_screen.dart (1)

45-48: 可提炼重复的“保存失败”提示为私有方法,减少重复代码(可选)

三处错误提示文案/逻辑一致,可本地封装,提升可维护性。

示例(放在 State 内,支持现有调用):

void _showSaveError(Object error) {
  if (!mounted) return;
  SnackBarHelper.showError(context, '保存失败:$error');
}

调用处替换为:_showSaveError(error.error);

Also applies to: 69-72, 93-96

lib/ui/core/ui/label_edit_dialog.dart (1)

238-243: 保存按钮未等待外部回调,可能导致调用方的提示在弹窗关闭后无法显示

当前直接调用 widget.onUpdateLabels(...); Navigator.pop(),未等待回调完成。若调用方在 Dialog 的 builder 上下文里显示 SnackBar(如 BookmarkDetailScreen 中的实现),Dialog 关闭会使该 context 立刻 unmount,从而导致回调后的成功/失败提示无法展示。

建议用“返回结果”模式,让 Dialog 通过 Navigator.pop(selectedLabels) 把数据交给外层,外层 await showDialog 后再执行更新并在页面级 context 中显示提示,避免 context 失效问题。

可以按需提供完整改造版实现(Dialog 返回值 + 外层 await)。

lib/ui/bookmarks/widget/bookmark_detail_screen.dart (2)

76-79: “标记喜爱”即时提示可能与最终状态不一致(轻微)

当前提示文案基于操作前的 isMarked 推断,并在异步操作前就弹出;若随后的命令失败,会先出现成功提示再出现失败提示,造成短暂矛盾。可考虑:

  • 等待命令成功后再提示;或
  • 使用命令结果事件决定提示内容。

保持现状也可接受(体验权衡)。


801-817: 硬编码颜色不符合样式规范,建议改用主题色(可选)

Colors.black/white 在图片预览页直接硬编码,不符合“所有样式通过主题提供”的项目约定。建议改为使用 Theme.of(context).colorScheme 中合适的色值(如 scrim / surface / onSurface 等),或在主题中新增专用色位。

如需,我可以给出具体替代方案并同步更新测试。

Also applies to: 812-817

lib/ui/core/ui/bookmark_card.dart (1)

274-278: 可选:展示归档 Toast 前加 mounted 检查,防止销毁后使用 context

极少数情况下(例如父级立即移除该组件),仍可能在销毁后调用到这里。加一行防护更稳妥。

-                              SnackBarHelper.showSuccess(
-                                context,
-                                widget.bookmark.isArchived ? '已取消归档' : '已标记归档',
-                                duration: const Duration(seconds: 2),
-                              );
+                              if (mounted) {
+                                SnackBarHelper.showSuccess(
+                                  context,
+                                  widget.bookmark.isArchived ? '已取消归档' : '已标记归档',
+                                  duration: const Duration(seconds: 2),
+                                );
+                              }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled
  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 5b90ab5 and a09412b.

📒 Files selected for processing (17)
  • CLAUDE.md (1 hunks)
  • lib/config/dependencies.dart (1 hunks)
  • lib/data/repository/bookmark/bookmark_repository.dart (3 hunks)
  • lib/ui/api_config/widgets/api_config_page.dart (2 hunks)
  • lib/ui/bookmarks/widget/bookmark_detail_screen.dart (7 hunks)
  • lib/ui/bookmarks/widget/bookmark_list_screen.dart (2 hunks)
  • lib/ui/core/theme.dart (2 hunks)
  • lib/ui/core/ui/bookmark_card.dart (4 hunks)
  • lib/ui/core/ui/label_edit_dialog.dart (2 hunks)
  • lib/ui/core/ui/snack_bar_helper.dart (1 hunks)
  • lib/ui/daily_read/widgets/daily_read_screen.dart (6 hunks)
  • lib/ui/settings/widgets/ai_settings_screen.dart (2 hunks)
  • lib/ui/settings/widgets/translation_settings_screen.dart (8 hunks)
  • test/ui/core/ui/bookmark_card_test.dart (1 hunks)
  • test/ui/core/ui/snack_bar_helper_test.dart (1 hunks)
  • test/unit/data/repository/bookmark/bookmark_repository_test.dart (1 hunks)
  • test/unit/data/repository/bookmark/bookmark_repository_test.mocks.dart (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • test/ui/core/ui/bookmark_card_test.dart
  • CLAUDE.md
🧰 Additional context used
📓 Path-based instructions (5)
lib/**/*.dart

📄 CodeRabbit Inference Engine (.trae/rules/project_rules.md)

所有样式必须通过主题提供,不允许硬编码颜色、字号等(例如禁止直接使用 Color/0xFF/固定 fontSize)

Files:

  • lib/ui/settings/widgets/ai_settings_screen.dart
  • lib/ui/settings/widgets/translation_settings_screen.dart
  • lib/ui/api_config/widgets/api_config_page.dart
  • lib/ui/core/ui/label_edit_dialog.dart
  • lib/ui/bookmarks/widget/bookmark_list_screen.dart
  • lib/ui/bookmarks/widget/bookmark_detail_screen.dart
  • lib/ui/core/theme.dart
  • lib/ui/core/ui/snack_bar_helper.dart
  • lib/data/repository/bookmark/bookmark_repository.dart
  • lib/ui/core/ui/bookmark_card.dart
  • lib/ui/daily_read/widgets/daily_read_screen.dart
  • lib/config/dependencies.dart
lib/ui/**/*[Ss]creen.dart

📄 CodeRabbit Inference Engine (.trae/rules/project_rules.md)

lib/ui/**/*[Ss]creen.dart: View(Screen)构造函数仅接受 key 与 viewModel;不包含任何业务逻辑;用户交互通过 Command 执行;处理 Command 的执行中/错误/完成状态
在 StatefulWidget 中通过 Command 监听器处理完成与错误:initState 中订阅 results/errors,dispose 中取消,使用 mounted 检查
所有错误状态应统一使用 ErrorPage 组件,优先通过工厂方法(如 ErrorPage.fromException)创建并遵循主题

Files:

  • lib/ui/settings/widgets/ai_settings_screen.dart
  • lib/ui/settings/widgets/translation_settings_screen.dart
  • lib/ui/bookmarks/widget/bookmark_list_screen.dart
  • lib/ui/bookmarks/widget/bookmark_detail_screen.dart
  • lib/ui/daily_read/widgets/daily_read_screen.dart
test/unit/data/**/*.dart

📄 CodeRabbit Inference Engine (.trae/rules/project_rules.md)

Repository 层需具备 100% 单元测试覆盖

Files:

  • test/unit/data/repository/bookmark/bookmark_repository_test.dart
  • test/unit/data/repository/bookmark/bookmark_repository_test.mocks.dart
lib/ui/core/theme.dart

📄 CodeRabbit Inference Engine (.trae/rules/project_rules.md)

集中定义应用主题配置于 lib/ui/core/theme.dart,统一管理颜色、字号、组件样式

Files:

  • lib/ui/core/theme.dart
lib/data/repository/**/*.dart

📄 CodeRabbit Inference Engine (.trae/rules/project_rules.md)

lib/data/repository/**/*.dart: Repository 不得继承 ChangeNotifier;使用 StreamController/Stream 对外通知数据变更
Repository 返回 Result,实现缓存、错误处理与重试;将 API 模型转换为领域模型;仅持有私有 Service 引用
Repository 充当特定数据类型的单一数据源(SSOT),避免重复与不一致

Files:

  • lib/data/repository/bookmark/bookmark_repository.dart
🧠 Learnings (14)
📚 Learning: 2025-08-13T18:04:36.250Z
Learnt from: CR
PR: shadowfish07/ReadeckAPP#0
File: .trae/rules/project_rules.md:0-0
Timestamp: 2025-08-13T18:04:36.250Z
Learning: Applies to lib/ui/**/*[Ss]creen.dart : View(Screen)构造函数仅接受 key 与 viewModel;不包含任何业务逻辑;用户交互通过 Command 执行;处理 Command 的执行中/错误/完成状态

Applied to files:

  • lib/ui/settings/widgets/ai_settings_screen.dart
  • lib/ui/settings/widgets/translation_settings_screen.dart
  • lib/ui/bookmarks/widget/bookmark_detail_screen.dart
  • lib/ui/daily_read/widgets/daily_read_screen.dart
📚 Learning: 2025-08-13T18:04:36.250Z
Learnt from: CR
PR: shadowfish07/ReadeckAPP#0
File: .trae/rules/project_rules.md:0-0
Timestamp: 2025-08-13T18:04:36.250Z
Learning: Applies to lib/**/*ViewModel.dart : 对外暴露的异步操作通过 flutter_command 的 Command 暴露;UI 状态采用 private setter + public getter

Applied to files:

  • lib/ui/settings/widgets/translation_settings_screen.dart
  • lib/ui/bookmarks/widget/bookmark_detail_screen.dart
  • lib/ui/daily_read/widgets/daily_read_screen.dart
📚 Learning: 2025-08-13T18:04:36.250Z
Learnt from: CR
PR: shadowfish07/ReadeckAPP#0
File: .trae/rules/project_rules.md:0-0
Timestamp: 2025-08-13T18:04:36.250Z
Learning: Applies to lib/ui/core/ui/error_page.dart : 必须提供统一错误页面组件 ErrorPage 于 lib/ui/core/ui/error_page.dart

Applied to files:

  • lib/ui/api_config/widgets/api_config_page.dart
  • lib/ui/bookmarks/widget/bookmark_list_screen.dart
  • lib/ui/bookmarks/widget/bookmark_detail_screen.dart
📚 Learning: 2025-08-13T18:04:36.250Z
Learnt from: CR
PR: shadowfish07/ReadeckAPP#0
File: .trae/rules/project_rules.md:0-0
Timestamp: 2025-08-13T18:04:36.250Z
Learning: Applies to lib/ui/**/*[Ss]creen.dart : 所有错误状态应统一使用 ErrorPage 组件,优先通过工厂方法(如 ErrorPage.fromException)创建并遵循主题

Applied to files:

  • lib/ui/api_config/widgets/api_config_page.dart
  • lib/ui/bookmarks/widget/bookmark_list_screen.dart
  • lib/ui/bookmarks/widget/bookmark_detail_screen.dart
  • lib/ui/daily_read/widgets/daily_read_screen.dart
📚 Learning: 2025-08-13T18:04:36.250Z
Learnt from: CR
PR: shadowfish07/ReadeckAPP#0
File: .trae/rules/project_rules.md:0-0
Timestamp: 2025-08-13T18:04:36.250Z
Learning: Applies to test/unit/data/**/*.dart : Repository 层需具备 100% 单元测试覆盖

Applied to files:

  • test/unit/data/repository/bookmark/bookmark_repository_test.dart
📚 Learning: 2025-08-13T18:04:36.250Z
Learnt from: CR
PR: shadowfish07/ReadeckAPP#0
File: .trae/rules/project_rules.md:0-0
Timestamp: 2025-08-13T18:04:36.250Z
Learning: Applies to test/widget/**/*.dart : 为关键 Widget 提供 UI 测试

Applied to files:

  • test/ui/core/ui/snack_bar_helper_test.dart
📚 Learning: 2025-08-13T18:04:36.250Z
Learnt from: CR
PR: shadowfish07/ReadeckAPP#0
File: .trae/rules/project_rules.md:0-0
Timestamp: 2025-08-13T18:04:36.250Z
Learning: Applies to test/unit/ui/**/*.dart : ViewModel 层需具备 100% 单元测试覆盖,并包含 Result 处理逻辑测试

Applied to files:

  • test/ui/core/ui/snack_bar_helper_test.dart
📚 Learning: 2025-08-13T18:04:36.250Z
Learnt from: CR
PR: shadowfish07/ReadeckAPP#0
File: .trae/rules/project_rules.md:0-0
Timestamp: 2025-08-13T18:04:36.250Z
Learning: Applies to lib/**/*ViewModel.dart : ViewModel 必须继承 ChangeNotifier

Applied to files:

  • lib/ui/bookmarks/widget/bookmark_detail_screen.dart
📚 Learning: 2025-08-13T18:04:36.250Z
Learnt from: CR
PR: shadowfish07/ReadeckAPP#0
File: .trae/rules/project_rules.md:0-0
Timestamp: 2025-08-13T18:04:36.250Z
Learning: Applies to lib/ui/**/*[Ss]creen.dart : 在 StatefulWidget 中通过 Command 监听器处理完成与错误:initState 中订阅 results/errors,dispose 中取消,使用 mounted 检查

Applied to files:

  • lib/ui/bookmarks/widget/bookmark_detail_screen.dart
📚 Learning: 2025-08-13T18:04:36.250Z
Learnt from: CR
PR: shadowfish07/ReadeckAPP#0
File: .trae/rules/project_rules.md:0-0
Timestamp: 2025-08-13T18:04:36.250Z
Learning: Applies to lib/ui/core/theme.dart : 集中定义应用主题配置于 lib/ui/core/theme.dart,统一管理颜色、字号、组件样式

Applied to files:

  • lib/ui/core/theme.dart
📚 Learning: 2025-08-13T18:04:36.250Z
Learnt from: CR
PR: shadowfish07/ReadeckAPP#0
File: .trae/rules/project_rules.md:0-0
Timestamp: 2025-08-13T18:04:36.250Z
Learning: 遵循 Material Design 3 与 Flutter 官方最佳实践进行设计与实现

Applied to files:

  • lib/ui/core/ui/snack_bar_helper.dart
📚 Learning: 2025-08-13T18:04:36.250Z
Learnt from: CR
PR: shadowfish07/ReadeckAPP#0
File: .trae/rules/project_rules.md:0-0
Timestamp: 2025-08-13T18:04:36.250Z
Learning: Applies to lib/data/repository/**/*.dart : Repository 不得继承 ChangeNotifier;使用 StreamController/Stream 对外通知数据变更

Applied to files:

  • lib/data/repository/bookmark/bookmark_repository.dart
📚 Learning: 2025-08-13T18:04:36.250Z
Learnt from: CR
PR: shadowfish07/ReadeckAPP#0
File: .trae/rules/project_rules.md:0-0
Timestamp: 2025-08-13T18:04:36.250Z
Learning: Applies to lib/data/repository/**/*.dart : Repository 充当特定数据类型的单一数据源(SSOT),避免重复与不一致

Applied to files:

  • lib/data/repository/bookmark/bookmark_repository.dart
📚 Learning: 2025-08-13T18:04:36.250Z
Learnt from: CR
PR: shadowfish07/ReadeckAPP#0
File: .trae/rules/project_rules.md:0-0
Timestamp: 2025-08-13T18:04:36.250Z
Learning: Applies to lib/**/*ViewModel.dart : ViewModel 通过私有 final 成员持有 Repository 引用(不直接依赖 Service),并在构造函数中初始化所有 Commands

Applied to files:

  • lib/ui/daily_read/widgets/daily_read_screen.dart
🔇 Additional comments (18)
lib/data/repository/bookmark/bookmark_repository.dart (1)

15-21: 引入 ArticleRepository 作为第三个依赖的改造方向正确

新增构造参数与私有字段对齐了下游行为(缺失阅读统计时补抓文章并计算保存),同时保持了 Repository 不继承 ChangeNotifier 的约束,符合项目规则。整体 LGTM。

(可选改进:根据项目规范“Repository 层使用 StreamController/Stream 对外通知变更”,后续可考虑将自定义监听器列表替换为 Stream,以提升可组合性与全局一致性。)

test/unit/data/repository/bookmark/bookmark_repository_test.mocks.dart (1)

1-410: 生成的 Mockito Mocks 文件无需人工评审

这是自动生成文件,类型与方法签名匹配被测代码,满足当前测试需求。

lib/ui/core/theme.dart (2)

38-46: 亮色主题的 SnackBar 配置整体合理

使用 ColorScheme 的 inverseSurface/onInverseSurface,行为为浮动,圆角与间距符合 M3 观感,满足“样式通过主题集中管理”的项目约束。


68-76: 暗色主题的 SnackBar 配置与亮色保持一致性,👍

同样遵循 ColorScheme,参数对齐,保证了深浅色外观一致。

lib/ui/core/ui/snack_bar_helper.dart (1)

3-71: API 设计清晰且语义化,符合 M3 与项目主题化约束

提供 success/error/info/warning 四种语义方法,色彩来源统一取自 Theme.of(context).colorScheme,默认时长区分合理,易用性好。

lib/ui/settings/widgets/ai_settings_screen.dart (1)

47-50: 改用 SnackBarHelper 展示错误 + mounted 守卫,做法正确

错误提示样式统一到 Helper,且在异步回调中检查 mounted,避免已卸载上下文导致的异常。

lib/ui/settings/widgets/translation_settings_screen.dart (1)

33-36: 将成功/错误/信息提示统一切换到 SnackBarHelper,符合主题化与复用目标

  • 成功与错误提示均在监听器中使用 mounted 检查,生命周期安全。
  • 文案保持不变,迁移风险低。

Also applies to: 45-48, 56-59, 69-72, 81-84, 93-96, 178-181

lib/ui/api_config/widgets/api_config_page.dart (1)

97-100: 保存失败改用 SnackBarHelper,且包含 context.mounted 检查,👍

错误提示与全局样式一致,避免了直接构造 SnackBar 的重复代码。

lib/ui/core/ui/label_edit_dialog.dart (2)

3-3: 引入 SnackBarHelper 统一提示样式 — LGTM

集中化消息提示有利于一致性与主题对齐。


73-76: 加载失败改用 SnackBarHelper — LGTM

异常信息透传到 UI,已配合 mounted 判定,行为合理。

lib/ui/bookmarks/widget/bookmark_list_screen.dart (2)

12-12: 引入 SnackBarHelper — LGTM

统一通知入口,符合项目风格与主题策略。


229-233: 错误提示改为 SnackBarHelper — LGTM

  • 保留原始错误信息,且有 3 秒时长,交互明确。
  • 已加 context.mounted,避免生命周期问题。
lib/ui/bookmarks/widget/bookmark_detail_screen.dart (3)

42-45: AI 翻译错误使用 SnackBarHelper 提示 — LGTM

含 mounted 判定,避免在已卸载时操作 UI。


685-689: 归档成功提示 — LGTM

与失败分支对称且有 mounted 检查,行为稳健。


708-711: 错误提示统一化 — LGTM

异常信息透传,便于问题定位。

lib/ui/daily_read/widgets/daily_read_screen.dart (1)

198-203: 更新标签失败提示改为 SnackBarHelper — LGTM

与其他页面一致,配合 context.mounted,可避免生命周期问题。

test/ui/core/ui/snack_bar_helper_test.dart (1)

1-246: SnackBarHelper 覆盖全面、断言到位 — LGTM

覆盖成功/错误/信息/警告,以及 Action 与时长、主题色断言,足以保护核心行为。

lib/ui/core/ui/bookmark_card.dart (1)

7-7: 引入 SnackBarHelper 统一反馈样式:赞,同步全局体验

集中封装反馈提示是对的方向,便于复用与统一主题风格。

Comment on lines +42 to +52
SnackBarHelper.showError(
context,
error.toString(),
action: SnackBarAction(
label: '复制链接',
onPressed: () async {
await Clipboard.setData(ClipboardData(text: widget.bookmark.url));
if (mounted) {
SnackBarHelper.showSuccess(context, '链接已复制到剪贴板');
}
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

修复:Stream.listen 回调签名错误,且未做 mounted 防护

当前 listen((error, _)) 的回调签名不符合 Stream.listenonData(T event) 约定(只接收一个参数),会导致编译错误或运行时异常。并且在异步回调里未先判断 mounted,组件销毁后仍可能尝试访问 context

建议最小修复如下:

-    widget.onOpenUrl.errors.where((x) => x != null).listen((error, _) {
-      SnackBarHelper.showError(
+    widget.onOpenUrl.errors.where((x) => x != null).listen((error) {
+      if (!mounted) return;
+      SnackBarHelper.showError(
         context,
         error.toString(),
         action: SnackBarAction(
           label: '复制链接',
           onPressed: () async {
             await Clipboard.setData(ClipboardData(text: widget.bookmark.url));
             if (mounted) {
               SnackBarHelper.showSuccess(context, '链接已复制到剪贴板');
             }
           },
         ),
       );
     });

补充建议(可选但推荐):

  • 避免重复订阅与内存泄漏:在 didChangeDependencies 里保存订阅句柄并在 dispose 里取消;每次依赖变更前先取消旧订阅再重新订阅。
  • 给订阅增加错误处理(若底层流可能 throw),或通过上游 onOpenUrl 统一封装为错误事件。

可在类中增加如下成员与生命周期管理(需要引入 dart:async):

// 需要添加:import 'dart:async';

class _BookmarkCardState extends State<BookmarkCard> {
  StreamSubscription? _openUrlErrorSub;

  @override
  void didChangeDependencies() {
    _openUrlErrorSub?.cancel();
    _openUrlErrorSub = widget.onOpenUrl.errors
        .where((x) => x != null)
        .listen((error) {
          if (!mounted) return;
          SnackBarHelper.showError(
            context,
            error.toString(),
            action: SnackBarAction(
              label: '复制链接',
              onPressed: () async {
                await Clipboard.setData(ClipboardData(text: widget.bookmark.url));
                if (mounted) {
                  SnackBarHelper.showSuccess(context, '链接已复制到剪贴板');
                }
              },
            ),
          );
        });
    super.didChangeDependencies();
  }

  @override
  void dispose() {
    _openUrlErrorSub?.cancel();
    super.dispose();
  }
}
🤖 Prompt for AI Agents
lib/ui/core/ui/bookmark_card.dart lines 42-52: the Stream.listen callback
currently uses an incorrect signature and doesn't guard against widget being
unmounted before showing a SnackBar; change the subscription to listen to a
single-argument onData handler (e.g., .listen((error) { ... })) and check
mounted before using context; additionally, store the StreamSubscription on the
State, cancel any previous subscription in didChangeDependencies before creating
a new one, and cancel the subscription in dispose to avoid leaks; keep the
SnackBar action logic but wrap all context usage behind if (!mounted) return or
if (mounted) checks.

@shadowfish07 shadowfish07 force-pushed the feature/label-edit-toast-feedback branch from a09412b to 817b173 Compare August 16, 2025 03:12
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🔭 Outside diff range comments (1)
lib/ui/daily_read/widgets/daily_read_screen.dart (1)

53-87: 监听注册位置与释放时机不当,建议移至 initState 并在 dispose 中取消订阅

当前在 didChangeDependencies 中多次 listen 且未显式取消,可能造成重复订阅、内存泄漏与重复弹 Toast。根据项目规约,建议在 initState 中订阅,在 dispose 中取消,并用 mounted 检查。

可以按此思路改造(示例,具体类型请以实际返回值为准):

// 成员变量
late final StreamSubscription _subLoadError;
late final StreamSubscription _subToggleArchiveError;
late final StreamSubscription _subToggleMarkedError;

@override
void initState() {
  super.initState();
  // ...已有初始化
  _subLoadError = widget.viewModel.load.errors.where((x) => x != null).listen((error, _) {
    appLogger.e('加载书签失败', error: error);
    if (!mounted) return;
    SnackBarHelper.showError(context, '加载书签失败');
  });
  _subToggleArchiveError = widget.viewModel.toggleBookmarkArchived.errors.where((x) => x != null).listen((error, _) {
    appLogger.e('切换书签归档状态失败', error: error);
    if (!mounted) return;
    SnackBarHelper.showError(context, '切换书签归档状态失败');
  });
  _subToggleMarkedError = widget.viewModel.toggleBookmarkMarked.errors.where((x) => x != null).listen((error, _) {
    appLogger.e('切换书签标记状态失败', error: error);
    if (!mounted) return;
    SnackBarHelper.showError(context, '切换书签标记状态失败');
  });
}

@override
void dispose() {
  _subLoadError.cancel();
  _subToggleArchiveError.cancel();
  _subToggleMarkedError.cancel();
  super.dispose();
}

若 .errors 的 listen 返回非 StreamSubscription(例如自定义 Disposer),也请持有返回值并在 dispose 调用其取消方法。

♻️ Duplicate comments (1)
lib/ui/daily_read/widgets/daily_read_screen.dart (1)

229-231: 首屏 Loading 判定仍可能漏显,建议使用“首次加载或手动刷新”即显示 Loading

此前已指出该问题:仅在 lastValue 非空且为空列表时显示 Loading,会导致首次加载(lastValue == null)直接渲染内容而非 Loading。建议如下修改:

-            if (lastValue != null && lastValue.isEmpty) {
+            // 首次加载(lastValue == null)或用户主动刷新(param == true)时展示 Loading
+            if (param == true || lastValue == null) {
               return const Loading(text: '正在加载今日推荐');
             }

如果需要更精细的控制,可在 ViewModel 暴露 isInitialLoading 或结合 load.isExecuting 判定。

🧹 Nitpick comments (5)
lib/data/repository/bookmark/bookmark_repository.dart (2)

3-3: 建议避免仓储依赖仓储,遵循“仅持有私有 Service 引用”规范

这里引入 ArticleRepository,意味着 BookmarkRepository 开始直接依赖另一仓储。结合现有对 ReadingStatsRepository 的依赖,这会进一步加深仓储之间的耦合,弱化各自作为单一数据源(SSOT)的边界,并带来循环依赖与演进成本的风险。

更稳妥的做法:

  • 将“拉取文章并计算阅读统计”的编排上移到 UseCase/DomainService 层,协调调用 ArticleRepository 与 ReadingStatsRepository;
  • 或将该编排下移到 ReadingStatsRepository(作为阅读统计的 SSOT),并让其依赖 ArticleService/ApiClient(而非 ArticleRepository)。

若短期内保留该依赖,建议至少以抽象接口(如 ArticleContentProvider)隔离直接仓储依赖,降低耦合。


111-139: 优化日志与取值:在未改变行为前先落地小改动

  • lib/data/repository/bookmark/bookmark_repository.dart 中,把对 articleResult 和 catch 块的处理改为:
    • articleResult.getOrNull()!articleResult.getOrThrow()
    • appLogger.e('处理书签 ${b.id} 的阅读统计数据时发生错误: $e')
      appLogger.e('处理书签 ${b.id} 的阅读统计数据时发生错误', error: e)
@@ lib/data/repository/bookmark/bookmark_repository.dart
-            if (articleResult.isSuccess()) {
-              final htmlContent = articleResult.getOrNull()!;
+            if (articleResult.isSuccess()) {
+              final htmlContent = articleResult.getOrThrow();
@@
-          } catch (e) {
-            appLogger.e('处理书签 ${b.id} 的阅读统计数据时发生错误: $e');
+          } catch (e) {
+            appLogger.e('处理书签 ${b.id} 的阅读统计数据时发生错误', error: e);

后续可选改进(后续 PR):

  • getReadingStats 返回的“未找到”场景定义专用异常/错误码,仅在该类型时才补算;其余错误直接返回失败。
  • 为网络抓取与计算添加重试(带退避)并限制并发,避免瞬时故障或大批量请求。
  • 增加性能监控与指标上报,帮助定位异常放大点。
    [optional_refactors_recommended]
test/ui/core/ui/bookmark_card_test.dart (1)

87-98: 小建议:使用定时 pump 替代 pumpAndSettle,以避免动画导致的潜在“不收敛”风险

SnackBar 为浮动并带进出场动画,pumpAndSettle 在某些主题/动画配置下可能不易收敛。可考虑使用带时长的 pump 来让动画推进到可断言的时间点。

例如:

await tester.tap(find.text('保存'));
// 推进一段时间供 SnackBar 入场动画完成
await tester.pump(const Duration(milliseconds: 300));

Also applies to: 120-127

lib/ui/core/ui/snack_bar_helper.dart (2)

82-100: 在缺少 ScaffoldMessenger 时安全降级,避免运行时异常

当上下文不在 Scaffold 范围内时,ScaffoldMessenger.of(context) 会抛异常。建议使用 maybeOf 并在为空时直接返回,增强健壮性。

-    ScaffoldMessenger.of(context).showSnackBar(
+    final messenger = ScaffoldMessenger.maybeOf(context);
+    if (messenger == null) return;
+    messenger.showSnackBar(
       SnackBar(
         content: Text(
           message,
           style: TextStyle(color: textColor),
         ),
         backgroundColor: backgroundColor,
         behavior: SnackBarBehavior.floating,
         margin: const EdgeInsets.all(16.0),
         shape: RoundedRectangleBorder(
           borderRadius: BorderRadius.circular(12.0),
         ),
         duration: duration,
         action: action?.copyWith(
           textColor: textColor,
         ),
       ),
-    );
+    );

82-100: 可选:显示新消息前清理当前 SnackBar,避免堆积

若短时间内重复触发多条消息,可能堆积队列。可考虑在 showSnackBar 前调用 clearSnackBars 或 hideCurrentSnackBar 实现替换式提示。

示例(与上一建议可合并):

messenger.clearSnackBars();
// 或 messenger.hideCurrentSnackBar();
messenger.showSnackBar(...);
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled
  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between a09412b and 817b173.

📒 Files selected for processing (17)
  • CLAUDE.md (1 hunks)
  • lib/config/dependencies.dart (1 hunks)
  • lib/data/repository/bookmark/bookmark_repository.dart (3 hunks)
  • lib/ui/api_config/widgets/api_config_page.dart (2 hunks)
  • lib/ui/bookmarks/widget/bookmark_detail_screen.dart (7 hunks)
  • lib/ui/bookmarks/widget/bookmark_list_screen.dart (2 hunks)
  • lib/ui/core/theme.dart (2 hunks)
  • lib/ui/core/ui/bookmark_card.dart (4 hunks)
  • lib/ui/core/ui/label_edit_dialog.dart (2 hunks)
  • lib/ui/core/ui/snack_bar_helper.dart (1 hunks)
  • lib/ui/daily_read/widgets/daily_read_screen.dart (5 hunks)
  • lib/ui/settings/widgets/ai_settings_screen.dart (2 hunks)
  • lib/ui/settings/widgets/translation_settings_screen.dart (8 hunks)
  • test/ui/core/ui/bookmark_card_test.dart (1 hunks)
  • test/ui/core/ui/snack_bar_helper_test.dart (1 hunks)
  • test/unit/data/repository/bookmark/bookmark_repository_test.dart (1 hunks)
  • test/unit/data/repository/bookmark/bookmark_repository_test.mocks.dart (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • CLAUDE.md
🚧 Files skipped from review as they are similar to previous changes (12)
  • lib/ui/settings/widgets/ai_settings_screen.dart
  • lib/ui/core/ui/label_edit_dialog.dart
  • lib/ui/api_config/widgets/api_config_page.dart
  • lib/ui/settings/widgets/translation_settings_screen.dart
  • lib/config/dependencies.dart
  • lib/ui/bookmarks/widget/bookmark_list_screen.dart
  • test/unit/data/repository/bookmark/bookmark_repository_test.dart
  • lib/ui/bookmarks/widget/bookmark_detail_screen.dart
  • test/unit/data/repository/bookmark/bookmark_repository_test.mocks.dart
  • test/ui/core/ui/snack_bar_helper_test.dart
  • lib/ui/core/theme.dart
  • lib/ui/core/ui/bookmark_card.dart
🧰 Additional context used
📓 Path-based instructions (3)
lib/**/*.dart

📄 CodeRabbit Inference Engine (.trae/rules/project_rules.md)

所有样式必须通过主题提供,不允许硬编码颜色、字号等(例如禁止直接使用 Color/0xFF/固定 fontSize)

Files:

  • lib/ui/core/ui/snack_bar_helper.dart
  • lib/ui/daily_read/widgets/daily_read_screen.dart
  • lib/data/repository/bookmark/bookmark_repository.dart
lib/ui/**/*[Ss]creen.dart

📄 CodeRabbit Inference Engine (.trae/rules/project_rules.md)

lib/ui/**/*[Ss]creen.dart: View(Screen)构造函数仅接受 key 与 viewModel;不包含任何业务逻辑;用户交互通过 Command 执行;处理 Command 的执行中/错误/完成状态
在 StatefulWidget 中通过 Command 监听器处理完成与错误:initState 中订阅 results/errors,dispose 中取消,使用 mounted 检查
所有错误状态应统一使用 ErrorPage 组件,优先通过工厂方法(如 ErrorPage.fromException)创建并遵循主题

Files:

  • lib/ui/daily_read/widgets/daily_read_screen.dart
lib/data/repository/**/*.dart

📄 CodeRabbit Inference Engine (.trae/rules/project_rules.md)

lib/data/repository/**/*.dart: Repository 不得继承 ChangeNotifier;使用 StreamController/Stream 对外通知数据变更
Repository 返回 Result,实现缓存、错误处理与重试;将 API 模型转换为领域模型;仅持有私有 Service 引用
Repository 充当特定数据类型的单一数据源(SSOT),避免重复与不一致

Files:

  • lib/data/repository/bookmark/bookmark_repository.dart
🧠 Learnings (8)
📚 Learning: 2025-08-13T18:04:36.250Z
Learnt from: CR
PR: shadowfish07/ReadeckAPP#0
File: .trae/rules/project_rules.md:0-0
Timestamp: 2025-08-13T18:04:36.250Z
Learning: 遵循 Material Design 3 与 Flutter 官方最佳实践进行设计与实现

Applied to files:

  • lib/ui/core/ui/snack_bar_helper.dart
📚 Learning: 2025-08-13T18:04:36.250Z
Learnt from: CR
PR: shadowfish07/ReadeckAPP#0
File: .trae/rules/project_rules.md:0-0
Timestamp: 2025-08-13T18:04:36.250Z
Learning: Applies to lib/ui/**/*[Ss]creen.dart : 所有错误状态应统一使用 ErrorPage 组件,优先通过工厂方法(如 ErrorPage.fromException)创建并遵循主题

Applied to files:

  • lib/ui/daily_read/widgets/daily_read_screen.dart
📚 Learning: 2025-08-13T18:04:36.250Z
Learnt from: CR
PR: shadowfish07/ReadeckAPP#0
File: .trae/rules/project_rules.md:0-0
Timestamp: 2025-08-13T18:04:36.250Z
Learning: Applies to lib/ui/**/*[Ss]creen.dart : View(Screen)构造函数仅接受 key 与 viewModel;不包含任何业务逻辑;用户交互通过 Command 执行;处理 Command 的执行中/错误/完成状态

Applied to files:

  • lib/ui/daily_read/widgets/daily_read_screen.dart
📚 Learning: 2025-08-13T18:04:36.250Z
Learnt from: CR
PR: shadowfish07/ReadeckAPP#0
File: .trae/rules/project_rules.md:0-0
Timestamp: 2025-08-13T18:04:36.250Z
Learning: Applies to test/widget/**/*.dart : 为关键 Widget 提供 UI 测试

Applied to files:

  • test/ui/core/ui/bookmark_card_test.dart
📚 Learning: 2025-08-13T18:04:36.250Z
Learnt from: CR
PR: shadowfish07/ReadeckAPP#0
File: .trae/rules/project_rules.md:0-0
Timestamp: 2025-08-13T18:04:36.250Z
Learning: Applies to test/integration/**/*.dart : 编写集成测试覆盖核心用户流程与 Command 执行链路

Applied to files:

  • test/ui/core/ui/bookmark_card_test.dart
📚 Learning: 2025-08-13T18:04:36.250Z
Learnt from: CR
PR: shadowfish07/ReadeckAPP#0
File: .trae/rules/project_rules.md:0-0
Timestamp: 2025-08-13T18:04:36.250Z
Learning: Applies to test/unit/ui/**/*.dart : ViewModel 层需具备 100% 单元测试覆盖,并包含 Result 处理逻辑测试

Applied to files:

  • test/ui/core/ui/bookmark_card_test.dart
📚 Learning: 2025-08-13T18:04:36.250Z
Learnt from: CR
PR: shadowfish07/ReadeckAPP#0
File: .trae/rules/project_rules.md:0-0
Timestamp: 2025-08-13T18:04:36.250Z
Learning: Applies to lib/data/repository/**/*.dart : Repository 不得继承 ChangeNotifier;使用 StreamController/Stream 对外通知数据变更

Applied to files:

  • lib/data/repository/bookmark/bookmark_repository.dart
📚 Learning: 2025-08-13T18:04:36.250Z
Learnt from: CR
PR: shadowfish07/ReadeckAPP#0
File: .trae/rules/project_rules.md:0-0
Timestamp: 2025-08-13T18:04:36.250Z
Learning: Applies to lib/data/repository/**/*.dart : Repository 充当特定数据类型的单一数据源(SSOT),避免重复与不一致

Applied to files:

  • lib/data/repository/bookmark/bookmark_repository.dart
🔇 Additional comments (10)
lib/data/repository/bookmark/bookmark_repository.dart (1)

15-20: 已确认 BookmarkRepository 的依赖注入与测试覆盖

  • lib/config/dependencies.dart 中的 Provider 已按三参数(ReadeckApiClient、ReadingStatsRepository、ArticleRepository)统一注入
  • 手动 new 场景(单测中的 BookmarkRepository 实例化)已同步使用三参构造
  • 单元测试中已生成并使用 MockArticleRepository,所有与 ArticleRepository 相关的用例均已注入/Mock

该改动无需额外调整。

lib/ui/daily_read/widgets/daily_read_screen.dart (2)

16-16: 引入 SnackBarHelper 很好,统一了反馈样式

集中式的 SnackBar 辅助对齐了主题与交互,减少了重复代码。与项目的主题化规则一致。


198-202: 标签更新失败的错误反馈符合预期

捕获错误并以 3 秒时长弹出错误 Toast,文案明确且合适。已做 context.mounted 防御,细节到位。

test/ui/core/ui/bookmark_card_test.dart (5)

80-101: 成功用例覆盖到位,验证了“保存”后成功 Toast 与回调触发

断言“标签已更新”与 SnackBar 出现,且回调被调用,符合交互预期。


103-127: 失败用例合理,断言错误 Toast 文案包含“更新标签失败”

通过抛出异常模拟失败路径,并验证 SnackBar 出现,覆盖了关键分支。


53-63: 基本可视元素与入口验证明确,提升了回归信心

按钮图标与 tooltip 的存在性检查简洁有效。


148-157: 更精确地定位了标签按钮启用态,细节到位

通过遍历 IconButton 并匹配具体图标确认 onPressed 非空,覆盖了线上的交互可用性。


216-228: 归档交互测试清晰,既验证回调触发也验证反馈 Toast

此用例对交互与反馈的双重验证很有价值。

lib/ui/core/ui/snack_bar_helper.dart (2)

6-21: 公共辅助封装设计合理,主题色使用规范

按类型选择 colorScheme 对应色值,统一了 SnackBar 视觉,符合“样式由主题提供”的原则。


103-115: SnackBarAction 的 copyWith 实用性强

保留标签与处理器,仅覆写 textColor,满足大多数统一样式场景。

Comment on lines +58 to 61
SnackBarHelper.showError(
context,
'加载书签失败',
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

在监听回调中缺少 mounted 检查,可能在组件已销毁后访问 context 导致异常

这些错误监听可能在页面销毁后触发。SnackBarHelper 内部会通过 Theme.of(context) 读取 InheritedWidget,若 context 已失效将抛出异常。建议在回调内先进行 mounted 检查。

请在以下位置加入 mounted 判断:

-      SnackBarHelper.showError(
-        context,
-        '加载书签失败',
-      );
+      if (!mounted) return;
+      SnackBarHelper.showError(
+        context,
+        '加载书签失败',
+      );
-      SnackBarHelper.showError(
-        context,
-        '切换书签归档状态失败',
-      );
+      if (!mounted) return;
+      SnackBarHelper.showError(
+        context,
+        '切换书签归档状态失败',
+      );
-      SnackBarHelper.showError(
-        context,
-        '切换书签标记状态失败',
-      );
+      if (!mounted) return;
+      SnackBarHelper.showError(
+        context,
+        '切换书签标记状态失败',
+      );

Also applies to: 70-73, 82-85

🤖 Prompt for AI Agents
In lib/ui/daily_read/widgets/daily_read_screen.dart around lines 58-61 (also
apply same fix to 70-73 and 82-85): the listeners call
SnackBarHelper.showError(context, ...) without checking mounted, which can
access an invalid context after the widget is disposed; update each listener
callback to first check if (!mounted) return; and only call
SnackBarHelper.showError when mounted is true so the context is valid before
using Theme.of(context) or other InheritedWidgets.

@codecov
Copy link

codecov bot commented Aug 16, 2025

Codecov Report

❌ Patch coverage is 42.30769% with 75 lines in your changes missing coverage. Please review.
✅ Project coverage is 23.58%. Comparing base (db47e18) to head (817b173).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
...ib/ui/bookmarks/widget/bookmark_detail_screen.dart 0.00% 21 Missing ⚠️
.../settings/widgets/translation_settings_screen.dart 0.00% 16 Missing ⚠️
lib/ui/core/theme.dart 0.00% 10 Missing ⚠️
lib/ui/core/ui/bookmark_card.dart 55.55% 8 Missing ⚠️
lib/ui/daily_read/widgets/daily_read_screen.dart 0.00% 8 Missing ⚠️
lib/ui/core/ui/label_edit_dialog.dart 0.00% 3 Missing ⚠️
lib/ui/settings/widgets/ai_settings_screen.dart 0.00% 3 Missing ⚠️
lib/ui/api_config/widgets/api_config_page.dart 0.00% 2 Missing ⚠️
lib/ui/bookmarks/widget/bookmark_list_screen.dart 0.00% 2 Missing ⚠️
lib/config/dependencies.dart 0.00% 1 Missing ⚠️
... and 1 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #69      +/-   ##
==========================================
+ Coverage   19.26%   23.58%   +4.32%     
==========================================
  Files          66       67       +1     
  Lines        3987     4036      +49     
==========================================
+ Hits          768      952     +184     
+ Misses       3219     3084     -135     
Flag Coverage Δ
unittests 23.58% <42.30%> (+4.32%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@shadowfish07 shadowfish07 merged commit 8664175 into main Aug 16, 2025
8 checks passed
@shadowfish07 shadowfish07 deleted the feature/label-edit-toast-feedback branch August 16, 2025 03:19
github-actions bot pushed a commit that referenced this pull request Aug 17, 2025
## [0.5.0](v0.4.1...v0.5.0) (2025-08-17)

### ✨ 新功能

* **bookmark:** 为标签编辑功能添加 toast 反馈 ([#69](#69)) ([8664175](8664175))
* **bookmark:** 空内容的书签点击后会直接浏览器打开 ([#70](#70)) ([68072d4](68072d4))
* 实现书签新建、分享新建功能 ([#73](#73)) ([f683506](f683506))
* 添加阅读中书签分类功能 ([#71](#71)) ([4caf993](4caf993)), closes [#19](#19)

### 🐛 Bug修复

* **bookmark:** 修复书签详情页HTML链接点击跳转错误问题 ([#68](#68)) ([db47e18](db47e18)), closes [#65](#65)

### ♻️ 代码重构

* **bookmark:** 引入书签显示模型整合阅读统计信息 ([b5cf7d9](b5cf7d9))
* **书签:** 重构书签模型处理和缓存逻辑 ([ec14388](ec14388))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

添加标签后增加反馈

2 participants