Browse Source

记忆中心客户端:拾忆页接服务端 + 写操作 UI + 本地提醒 + 每日播报

dart analyze 零错误,flutter build apk --debug 通过。

零期修缮
- tabTodo 补齐 41 个语言。此前只有 11 个文件有这个 key,其中 8 个的值是
  'Remin'(不是任何语言里的词,像 Reminisce 打了一半),缺 key 的 30 个经 GetX
  回退到 en_US 同样显示 'Remin' —— 41 个语言里 38 个是坏的,只是不报错。
- i18n_audit.py 补「缺失 key」检查。它此前**结构上无法发现缺失的 key**:循环遍历
  的是该文件已有的 key,且对 baseline 里没有的直接 continue。这正是 tabTodo 在 30
  个语言里悄悄丢掉而没人发现的原因。现以 zh_cn 为基准,实测报出 7243 处历史缺失
  (40 个语言只有 1 个不缺)。加了 --fail-on-missing/--allow-missing 给 CI 卡「只增
  不减」,默认不开——一上来就红会让人直接把检查关掉。
- pubspec 引入 timezone + flutter_timezone。zonedSchedule 只接受 tz.TZDateTime,
  没有这两个依赖,提醒功能一行都写不了。

拾忆页从纯展示改成可写(此前最大的工作量遗漏)
页面原来没有 RefreshIndicator、没有 FloatingActionButton、卡片没有 onTap,
于是服务端的 add/update/del/complete 四个写接口在客户端一个调用点都没有,
user_edited 那套「保护用户修改」的机制完全空转。本次补齐:
- MemoryEditSheet:新增/编辑弹层,按分类分流表单(统一表存储 ≠ 统一表单)
- 卡片 onTap 打开编辑、左侧勾选框标记完成(灵感不给勾选框,它没有完成一说)
- 右下角 + 新增、RefreshIndicator 下拉刷新、带引导的空态页
- date_certain=false 的项单独归到「待定日期」小节。一次会抽出 8 条没写截止时间的
  待办全堆在会议当天,不分开那一格就没法看了。

数据层
- MemoryItem / MemoryRemindSlot / MemoryReport 手写镜像服务端 pb。
  **每个字段都经宽松解析器**(memInt/memBool/memStr),不直接 as int:
  服务端删字段或老记录缺列时,non-nullable 会让整个 fromJson 抛 TypeError,
  表现是整页空白而不是少一个字段(appconfig_model 已经为此踩过坑)。
  单条解析失败只跳过那一条。
- 分类保留原始字符串而不是 enum 解析。服务端加分类只改常量不发包,
  客户端 switch 不到就抛等于服务端一加分类就打崩所有存量 App。
  CalendarEventCategory 加了 other 作为未知兜底,筛选与卡片分流都改成值比较
  而不是穷举 switch。
- MemoryService 只缓存最近 90 天。GetStorage 是把整个 box 读进内存的 JSON,
  一年三千多条全量缓存会让每次启动越来越慢。
- 拉取失败保留本地缓存并标 staleFromCache,页面显示行内「当前显示的是本地缓存」,
  不清空、不弹红条。
- 勾选完成是乐观更新 + 失败回滚。不回滚会让用户以为已经勾上,下次进页面又变回来。
- memory_today / memory_upcoming 加进 AuthInterceptor 的红条抑制名单:
  它们在启动与前台恢复时自动调,用户什么都没点就吃一条英文内部状态词很差。

删除 mock
mockCalendarEvents() 与它引用的 44 个 demo* i18n key 一并删除(41 个文件共 484 行)。
真实数据上线后混着演示数据会让人以为系统出错。

本地提醒(MemoryReminderScheduler)
- 通知 id 占 920000 起的固定区间,**逐条 cancel,绝不 cancelAll()** —— 仓库里已有
  两个各自 new 的 FlutterLocalNotificationsPlugin 实例,cancelAll 会顺手干掉正在
  录音的前台通知,表现是录音悄悄失去前台身份被系统回收。
- 单独的通知通道:录音提示是常驻低优先级通道,共用会让提醒被降级成静默。
- **只查权限不申请**。申请必须走 PermissionUtil(它收敛了过审要求的 48 小时冷却),
  且只允许用户在权限管理页主动发起,这里主动申请等于绕过合规规则。
  无权限时降级为「打开 App 才提醒」,不阻塞。
- 精确排期失败(Android 14 起 SCHEDULE_EXACT_ALARM 默认不授予)自动退化成
  inexact 再试一次——不精确的提醒也比没有强,日志会记下来。
- 拿不到系统时区时按本机偏移找等价区兜底,不落回 UTC(那会让国内用户全部偏 8 小时)。

每日播报(MemoryDailySheet)
- **不 await 在启动路径上**。冷启动已串四次网络往返,仓库刚做过相反方向的优化
  (6a64c3d0 把会议模板拉取改成 unawaited)。这里是进主页后异步拉、拿到再弹。
- 排在隐私政策与强更弹窗之后,且必须在登录态确认之后。
- 通话翻译进行中一律不播报:两者共用同一个 AVAudioSession,在对方刚调好的会话上
  再设一次会把通话音频路由弄乱(iOS 那条 phone_call 分支被改坏过三次)。
- 关窗必须真停:speakStream 是队列式播放,只 stop 不清队列会「关了还在念下一句」。
- 播报文案本地拼,不调 LLM。

生命周期与账号
- 切前台的刷新挂在 AppLifecycleManager(仓库已有 emai/translation/permissions
  三个各自实现的 observer,本类就是收敛点),全部 unawaited。
- 登出清空记忆数据与已排提醒。不清的话换账号会看到上一个人的日程花销灵感,
  提醒也会继续按上一个账号的安排响。

助手闹钟迁移
SET_clock 改为双写:本地 reminders 保留一个版本(上传失败时本机仍可用),
同时写服务端 memory_item(category=alarm)。**幂等键用指令 id** —— 本类的按 id
去重只在本进程内存里,换机/重装/重启后全失效,服务端靠 (uid, client_key)
唯一索引兜住。认不出的重复规则按单次处理,宁可只响一次也不按猜的规则天天响。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
main
Rodger-Wang 1 month ago
parent
commit
66463e53f6
  1. 8
      apps/client/lib/core/bindings/initial_binding.dart
  2. 55
      apps/client/lib/core/translations/language/ar_sa.dart
  3. 55
      apps/client/lib/core/translations/language/bg_bg.dart
  4. 55
      apps/client/lib/core/translations/language/cs_cz.dart
  5. 55
      apps/client/lib/core/translations/language/da_dk.dart
  6. 100
      apps/client/lib/core/translations/language/de_de.dart
  7. 55
      apps/client/lib/core/translations/language/el_gr.dart
  8. 100
      apps/client/lib/core/translations/language/en_us.dart
  9. 100
      apps/client/lib/core/translations/language/es_es.dart
  10. 55
      apps/client/lib/core/translations/language/et_ee.dart
  11. 55
      apps/client/lib/core/translations/language/fa_ir.dart
  12. 55
      apps/client/lib/core/translations/language/fi_fi.dart
  13. 100
      apps/client/lib/core/translations/language/fr_fr.dart
  14. 55
      apps/client/lib/core/translations/language/hi_in.dart
  15. 55
      apps/client/lib/core/translations/language/hr_hr.dart
  16. 55
      apps/client/lib/core/translations/language/hu_hu.dart
  17. 55
      apps/client/lib/core/translations/language/id_id.dart
  18. 55
      apps/client/lib/core/translations/language/is_is.dart
  19. 55
      apps/client/lib/core/translations/language/it_it.dart
  20. 100
      apps/client/lib/core/translations/language/ja_jp.dart
  21. 100
      apps/client/lib/core/translations/language/ko_kr.dart
  22. 55
      apps/client/lib/core/translations/language/lt_lt.dart
  23. 55
      apps/client/lib/core/translations/language/lv_lv.dart
  24. 55
      apps/client/lib/core/translations/language/ms_my.dart
  25. 55
      apps/client/lib/core/translations/language/nb_no.dart
  26. 55
      apps/client/lib/core/translations/language/nl_nl.dart
  27. 55
      apps/client/lib/core/translations/language/pl_pl.dart
  28. 100
      apps/client/lib/core/translations/language/pt_pt.dart
  29. 55
      apps/client/lib/core/translations/language/ro_ro.dart
  30. 100
      apps/client/lib/core/translations/language/ru_ru.dart
  31. 55
      apps/client/lib/core/translations/language/sk_sk.dart
  32. 55
      apps/client/lib/core/translations/language/sl_si.dart
  33. 55
      apps/client/lib/core/translations/language/sr_rs.dart
  34. 55
      apps/client/lib/core/translations/language/sv_se.dart
  35. 55
      apps/client/lib/core/translations/language/th_th.dart
  36. 55
      apps/client/lib/core/translations/language/tl_ph.dart
  37. 55
      apps/client/lib/core/translations/language/tr_tr.dart
  38. 55
      apps/client/lib/core/translations/language/uk_ua.dart
  39. 55
      apps/client/lib/core/translations/language/vi_vn.dart
  40. 98
      apps/client/lib/core/translations/language/zh_cn.dart
  41. 98
      apps/client/lib/core/translations/language/zh_hk.dart
  42. 98
      apps/client/lib/core/translations/language/zh_tw.dart
  43. 35
      apps/client/lib/core/utils/app_lifecycle_manager.dart
  44. 396
      apps/client/lib/data/models/memory_item.dart
  45. 18
      apps/client/lib/data/services/account_service.dart
  46. 73
      apps/client/lib/data/services/assistant_directive_service.dart
  47. 254
      apps/client/lib/data/services/memory_reminder_scheduler.dart
  48. 428
      apps/client/lib/data/services/memory_service.dart
  49. 103
      apps/client/lib/data/services/network/api.dart
  50. 7
      apps/client/lib/data/services/network/auth_interceptor.dart
  51. 145
      apps/client/lib/modules/home/controllers/calendar_controller.dart
  52. 240
      apps/client/lib/modules/home/views/widgets/calendar/calendar_event.dart
  53. 65
      apps/client/lib/modules/main_tab/views/tabs/todo_tab.dart
  54. 271
      apps/client/lib/modules/main_tab/views/widgets/memory_daily_sheet.dart
  55. 495
      apps/client/lib/modules/main_tab/views/widgets/memory_edit_sheet.dart
  56. 241
      apps/client/lib/modules/main_tab/views/widgets/todo_scope_list.dart
  57. 14
      apps/client/pubspec.lock
  58. 5
      apps/client/pubspec.yaml
  59. 42
      apps/client/tools/i18n_audit.py

8
apps/client/lib/core/bindings/initial_binding.dart

@ -33,6 +33,8 @@ import '/data/services/bes_call_recording_service.dart';
import '/data/services/face_to_face_ble_service.dart';
import '../services/ai_orb_controller.dart';
import '../../data/services/account_service.dart';
import '../../data/services/memory_reminder_scheduler.dart';
import '../../data/services/memory_service.dart';
import '../../data/services/version_update_service.dart';
import '../../data/services/ota_check_service.dart';
import '../../core/utils/app_lifecycle_manager.dart';
@ -46,6 +48,12 @@ class InitialBinding extends Bindings {
// 应用生命周期管理器
Get.put<AppLifecycleManager>(AppLifecycleManager(), permanent: true);
// 记忆中心(拾忆)。permanent:拾忆页、每日弹窗、提醒排期都要读同一份数据,
// 且 onInit 里要恢复本地缓存,不能随页面销毁。
Get.put<MemoryService>(MemoryService(), permanent: true);
Get.put<MemoryReminderScheduler>(MemoryReminderScheduler(),
permanent: true);
// 全局悬浮 AI 球:main.dart 的 builder 里马上要用,必须提前 put(不能 lazyPut)
Get.put<AiOrbController>(AiOrbController(), permanent: true);

55
apps/client/lib/core/translations/language/ar_sa.dart

@ -1,5 +1,60 @@
//阿拉伯-沙特阿拉伯语
const Map<String, String> arSA = {
'memDailyCount': 'You have @n item(s) today.',
'memCatAll': 'All',
'memCatTodo': 'To-do',
'memCatAlarm': 'Alarm',
'memCatIdea': 'Idea',
'memCatExpense': 'Expense',
'memCatOther': 'Other',
'memRepeatOnce': 'Once',
'memRepeatDaily': 'Daily',
'memRepeatWeekly': 'Weekly',
'memRepeatWeekdays': 'Weekdays',
'memRepeatWeekend': 'Weekends',
'memRepeatMonthly': 'Monthly',
'memEmptyTitle': 'Nothing recorded for this day',
'memEmptySubtitle': 'Tap + to add one, or ask EMAI to note it for you',
'memAddTitle': 'New',
'memEditTitle': 'Edit',
'memDelete': 'Delete',
'memDeleteConfirm': 'Delete this entry?',
'memFieldTitle': 'Content',
'memFieldDetail': 'Notes',
'memFieldDate': 'Date',
'memFieldTime': 'Time',
'memFieldAmount': 'Amount',
'memFieldOwner': 'Owner',
'memFieldRepeat': 'Repeat',
'memFieldRemind': 'Remind me',
'memRemindOff': 'No reminder',
'memRemindMinutes': '@n minutes before',
'memTitleRequired': 'Content cannot be empty',
'memAmountInvalid': 'Invalid amount',
'memSaveFailed': 'Save failed, please try again',
'memDeleteFailed': 'Delete failed, please try again',
'memCompleteFailed': 'Action failed, please try again',
'memOffline': 'Showing locally cached data',
'memFromMeeting': 'From meeting',
'memDatePending': 'Date to be set',
'memDueRaw': 'Due: @raw',
'memSummaryTodo': '@n to-do',
'memSummaryIdea': '@n idea',
'memSummaryAlarm': '@n alarm',
'memSummaryExpense': 'Spent @amount',
'memReminderTitle': 'Reminder',
'memNotifyDisabled': 'Notifications are off; reminders will only show while the app is open',
'memGoPermission': 'Settings',
'memReportWeekTitle': 'Last week',
'memReportMonthTitle': 'Last month',
'memReportConfirm': 'Got it',
'memReportPlay': 'Play',
'memReportStop': 'Stop',
'memDailyTitle': 'Today\'s plan',
'memDailyPlay': 'Play',
'memDailyEmpty': 'Nothing planned today',
'memDailyClose': 'Close',
'tabTodo': 'ذكريات',
// 通用
'continue': 'متابعة',
'confirm': 'تأكيد',

55
apps/client/lib/core/translations/language/bg_bg.dart

@ -1,5 +1,60 @@
//保加利亚-保加利亚语
const Map<String, String> bgBG = {
'memDailyCount': 'You have @n item(s) today.',
'memCatAll': 'All',
'memCatTodo': 'To-do',
'memCatAlarm': 'Alarm',
'memCatIdea': 'Idea',
'memCatExpense': 'Expense',
'memCatOther': 'Other',
'memRepeatOnce': 'Once',
'memRepeatDaily': 'Daily',
'memRepeatWeekly': 'Weekly',
'memRepeatWeekdays': 'Weekdays',
'memRepeatWeekend': 'Weekends',
'memRepeatMonthly': 'Monthly',
'memEmptyTitle': 'Nothing recorded for this day',
'memEmptySubtitle': 'Tap + to add one, or ask EMAI to note it for you',
'memAddTitle': 'New',
'memEditTitle': 'Edit',
'memDelete': 'Delete',
'memDeleteConfirm': 'Delete this entry?',
'memFieldTitle': 'Content',
'memFieldDetail': 'Notes',
'memFieldDate': 'Date',
'memFieldTime': 'Time',
'memFieldAmount': 'Amount',
'memFieldOwner': 'Owner',
'memFieldRepeat': 'Repeat',
'memFieldRemind': 'Remind me',
'memRemindOff': 'No reminder',
'memRemindMinutes': '@n minutes before',
'memTitleRequired': 'Content cannot be empty',
'memAmountInvalid': 'Invalid amount',
'memSaveFailed': 'Save failed, please try again',
'memDeleteFailed': 'Delete failed, please try again',
'memCompleteFailed': 'Action failed, please try again',
'memOffline': 'Showing locally cached data',
'memFromMeeting': 'From meeting',
'memDatePending': 'Date to be set',
'memDueRaw': 'Due: @raw',
'memSummaryTodo': '@n to-do',
'memSummaryIdea': '@n idea',
'memSummaryAlarm': '@n alarm',
'memSummaryExpense': 'Spent @amount',
'memReminderTitle': 'Reminder',
'memNotifyDisabled': 'Notifications are off; reminders will only show while the app is open',
'memGoPermission': 'Settings',
'memReportWeekTitle': 'Last week',
'memReportMonthTitle': 'Last month',
'memReportConfirm': 'Got it',
'memReportPlay': 'Play',
'memReportStop': 'Stop',
'memDailyTitle': 'Today\'s plan',
'memDailyPlay': 'Play',
'memDailyEmpty': 'Nothing planned today',
'memDailyClose': 'Close',
'tabTodo': 'Спомени',
// 通用
'continue': 'Продължи',
'confirm': 'Потвърди',

55
apps/client/lib/core/translations/language/cs_cz.dart

@ -1,5 +1,60 @@
//捷克-捷克语
const Map<String, String> csCZ = {
'memDailyCount': 'You have @n item(s) today.',
'memCatAll': 'All',
'memCatTodo': 'To-do',
'memCatAlarm': 'Alarm',
'memCatIdea': 'Idea',
'memCatExpense': 'Expense',
'memCatOther': 'Other',
'memRepeatOnce': 'Once',
'memRepeatDaily': 'Daily',
'memRepeatWeekly': 'Weekly',
'memRepeatWeekdays': 'Weekdays',
'memRepeatWeekend': 'Weekends',
'memRepeatMonthly': 'Monthly',
'memEmptyTitle': 'Nothing recorded for this day',
'memEmptySubtitle': 'Tap + to add one, or ask EMAI to note it for you',
'memAddTitle': 'New',
'memEditTitle': 'Edit',
'memDelete': 'Delete',
'memDeleteConfirm': 'Delete this entry?',
'memFieldTitle': 'Content',
'memFieldDetail': 'Notes',
'memFieldDate': 'Date',
'memFieldTime': 'Time',
'memFieldAmount': 'Amount',
'memFieldOwner': 'Owner',
'memFieldRepeat': 'Repeat',
'memFieldRemind': 'Remind me',
'memRemindOff': 'No reminder',
'memRemindMinutes': '@n minutes before',
'memTitleRequired': 'Content cannot be empty',
'memAmountInvalid': 'Invalid amount',
'memSaveFailed': 'Save failed, please try again',
'memDeleteFailed': 'Delete failed, please try again',
'memCompleteFailed': 'Action failed, please try again',
'memOffline': 'Showing locally cached data',
'memFromMeeting': 'From meeting',
'memDatePending': 'Date to be set',
'memDueRaw': 'Due: @raw',
'memSummaryTodo': '@n to-do',
'memSummaryIdea': '@n idea',
'memSummaryAlarm': '@n alarm',
'memSummaryExpense': 'Spent @amount',
'memReminderTitle': 'Reminder',
'memNotifyDisabled': 'Notifications are off; reminders will only show while the app is open',
'memGoPermission': 'Settings',
'memReportWeekTitle': 'Last week',
'memReportMonthTitle': 'Last month',
'memReportConfirm': 'Got it',
'memReportPlay': 'Play',
'memReportStop': 'Stop',
'memDailyTitle': 'Today\'s plan',
'memDailyPlay': 'Play',
'memDailyEmpty': 'Nothing planned today',
'memDailyClose': 'Close',
'tabTodo': 'Vzpomínky',
// 通用
'continue': 'Pokračovat',
'confirm': 'Potvrdit',

55
apps/client/lib/core/translations/language/da_dk.dart

@ -1,5 +1,60 @@
//丹麦-丹麦语
const Map<String, String> daDK = {
'memDailyCount': 'You have @n item(s) today.',
'memCatAll': 'All',
'memCatTodo': 'To-do',
'memCatAlarm': 'Alarm',
'memCatIdea': 'Idea',
'memCatExpense': 'Expense',
'memCatOther': 'Other',
'memRepeatOnce': 'Once',
'memRepeatDaily': 'Daily',
'memRepeatWeekly': 'Weekly',
'memRepeatWeekdays': 'Weekdays',
'memRepeatWeekend': 'Weekends',
'memRepeatMonthly': 'Monthly',
'memEmptyTitle': 'Nothing recorded for this day',
'memEmptySubtitle': 'Tap + to add one, or ask EMAI to note it for you',
'memAddTitle': 'New',
'memEditTitle': 'Edit',
'memDelete': 'Delete',
'memDeleteConfirm': 'Delete this entry?',
'memFieldTitle': 'Content',
'memFieldDetail': 'Notes',
'memFieldDate': 'Date',
'memFieldTime': 'Time',
'memFieldAmount': 'Amount',
'memFieldOwner': 'Owner',
'memFieldRepeat': 'Repeat',
'memFieldRemind': 'Remind me',
'memRemindOff': 'No reminder',
'memRemindMinutes': '@n minutes before',
'memTitleRequired': 'Content cannot be empty',
'memAmountInvalid': 'Invalid amount',
'memSaveFailed': 'Save failed, please try again',
'memDeleteFailed': 'Delete failed, please try again',
'memCompleteFailed': 'Action failed, please try again',
'memOffline': 'Showing locally cached data',
'memFromMeeting': 'From meeting',
'memDatePending': 'Date to be set',
'memDueRaw': 'Due: @raw',
'memSummaryTodo': '@n to-do',
'memSummaryIdea': '@n idea',
'memSummaryAlarm': '@n alarm',
'memSummaryExpense': 'Spent @amount',
'memReminderTitle': 'Reminder',
'memNotifyDisabled': 'Notifications are off; reminders will only show while the app is open',
'memGoPermission': 'Settings',
'memReportWeekTitle': 'Last week',
'memReportMonthTitle': 'Last month',
'memReportConfirm': 'Got it',
'memReportPlay': 'Play',
'memReportStop': 'Stop',
'memDailyTitle': 'Today\'s plan',
'memDailyPlay': 'Play',
'memDailyEmpty': 'Nothing planned today',
'memDailyClose': 'Close',
'tabTodo': 'Minder',
// 通用
'continue': 'Fortsæt',
'confirm': 'Bekræft',

100
apps/client/lib/core/translations/language/de_de.dart

@ -1,5 +1,59 @@
//德语-德语
const Map<String, String> deDE = {
'memDailyCount': 'You have @n item(s) today.',
'memCatAll': 'All',
'memCatTodo': 'To-do',
'memCatAlarm': 'Alarm',
'memCatIdea': 'Idea',
'memCatExpense': 'Expense',
'memCatOther': 'Other',
'memRepeatOnce': 'Once',
'memRepeatDaily': 'Daily',
'memRepeatWeekly': 'Weekly',
'memRepeatWeekdays': 'Weekdays',
'memRepeatWeekend': 'Weekends',
'memRepeatMonthly': 'Monthly',
'memEmptyTitle': 'Nothing recorded for this day',
'memEmptySubtitle': 'Tap + to add one, or ask EMAI to note it for you',
'memAddTitle': 'New',
'memEditTitle': 'Edit',
'memDelete': 'Delete',
'memDeleteConfirm': 'Delete this entry?',
'memFieldTitle': 'Content',
'memFieldDetail': 'Notes',
'memFieldDate': 'Date',
'memFieldTime': 'Time',
'memFieldAmount': 'Amount',
'memFieldOwner': 'Owner',
'memFieldRepeat': 'Repeat',
'memFieldRemind': 'Remind me',
'memRemindOff': 'No reminder',
'memRemindMinutes': '@n minutes before',
'memTitleRequired': 'Content cannot be empty',
'memAmountInvalid': 'Invalid amount',
'memSaveFailed': 'Save failed, please try again',
'memDeleteFailed': 'Delete failed, please try again',
'memCompleteFailed': 'Action failed, please try again',
'memOffline': 'Showing locally cached data',
'memFromMeeting': 'From meeting',
'memDatePending': 'Date to be set',
'memDueRaw': 'Due: @raw',
'memSummaryTodo': '@n to-do',
'memSummaryIdea': '@n idea',
'memSummaryAlarm': '@n alarm',
'memSummaryExpense': 'Spent @amount',
'memReminderTitle': 'Reminder',
'memNotifyDisabled': 'Notifications are off; reminders will only show while the app is open',
'memGoPermission': 'Settings',
'memReportWeekTitle': 'Last week',
'memReportMonthTitle': 'Last month',
'memReportConfirm': 'Got it',
'memReportPlay': 'Play',
'memReportStop': 'Stop',
'memDailyTitle': 'Today\'s plan',
'memDailyPlay': 'Play',
'memDailyEmpty': 'Nothing planned today',
'memDailyClose': 'Close',
// 通用
'continue': 'Fortfahren',
'confirm': 'Bestätigen',
@ -1797,7 +1851,7 @@ const Map<String, String> deDE = {
'currentConnection': 'Aktuelle Verbindung',
'binding': 'Wird verbunden…',
'tabDevice': 'Geräte',
'tabTodo': 'Remin',
'tabTodo': 'Erinnerungen',
'tabPlaza': 'Plaza',
'sloganHome': 'Technik für ein smarteres Leben',
'sloganAi': 'Grenzenlose Intelligenz',
@ -1949,50 +2003,6 @@ const Map<String, String> deDE = {
'recordPause': 'Pause',
'recordResume': 'Fortsetzen',
'recordDeleteConfirm': 'Diese Aufnahme löschen?',
'demoEvt1': 'Englisch am Morgen',
'demoEvt2': 'Wöchentliches Projekt-Meeting',
'demoEvt3': 'Wasser trinken',
'demoEvt4': 'Wochenbericht abgeben',
'demoEvt5': 'Training: Brust & Trizeps',
'demoEvt6': 'Lesen: Deep Work',
'demoEvt7': 'Zahnarzt-Nachkontrolle',
'demoEvt8': 'Frühstück',
'demoEvt9': 'Mit dem Taxi zur Arbeit',
'demoEvt10': 'Handyguthaben aufladen',
'demoEvt11': 'Kinokarten',
'demoEvt12': 'Buch kaufen',
'demoDesc1': '20 Minuten Shadowing, Fokus auf Aussprache.',
'demoDesc2': 'Wochenrückblick und Planung der nächsten Woche.',
'demoDesc3': 'Alle zwei Stunden.',
'demoDesc4': 'An das Team-Postfach gesendet.',
'demoDesc5': '10 Min. Aufwärmen, 40 Min. Arbeitssätze.',
'demoDesc6': 'Kapitel 3 zu Ende lesen und Notizen machen.',
'demoDesc7': 'Den letzten Befund mitnehmen.',
'demoDesc8': 'Sojamilch und Dampfbrötchen.',
'demoDesc9': 'Taxi zur Hauptverkehrszeit, um die volle U-Bahn zu vermeiden.',
'demoDesc10': 'Monatliche automatische Aufladung.',
'demoDesc11': 'Spätvorstellung mit Freunden.',
'demoDesc12': 'Deep Work als Printausgabe.',
'demoLoc1': 'Arbeitszimmer',
'demoLoc2': 'Besprechungsraum A',
'demoLoc4': 'Arbeitsplatz',
'demoLoc5': 'Fitnessstudio',
'demoLoc7': 'Zahnklinik',
'demoLoc8': 'Frühstückscafé unten',
'demoLoc9': 'Fahrdienst-App',
'demoLoc10': 'App des Anbieters',
'demoLoc11': 'Kino',
'demoLoc12': 'Onlinebuchhandlung',
'demoTagStudy': 'Lernen',
'demoTagMeeting': 'Meeting',
'demoTagReminder': 'Erinnerung',
'demoTagTask': 'Aufgabe',
'demoTagSport': 'Sport',
'demoTagBooking': 'Termin',
'demoTagFood': 'Essen',
'demoTagTransit': 'Verkehr',
'demoTagBills': 'Rechnungen',
'demoTagFun': 'Freizeit',
'scopeDay': 'Tag',
'scopeWeek': 'Woche',
'scopeMonth': 'Monat',

55
apps/client/lib/core/translations/language/el_gr.dart

@ -1,5 +1,60 @@
//希腊-希腊语
const Map<String, String> elGR = {
'memDailyCount': 'You have @n item(s) today.',
'memCatAll': 'All',
'memCatTodo': 'To-do',
'memCatAlarm': 'Alarm',
'memCatIdea': 'Idea',
'memCatExpense': 'Expense',
'memCatOther': 'Other',
'memRepeatOnce': 'Once',
'memRepeatDaily': 'Daily',
'memRepeatWeekly': 'Weekly',
'memRepeatWeekdays': 'Weekdays',
'memRepeatWeekend': 'Weekends',
'memRepeatMonthly': 'Monthly',
'memEmptyTitle': 'Nothing recorded for this day',
'memEmptySubtitle': 'Tap + to add one, or ask EMAI to note it for you',
'memAddTitle': 'New',
'memEditTitle': 'Edit',
'memDelete': 'Delete',
'memDeleteConfirm': 'Delete this entry?',
'memFieldTitle': 'Content',
'memFieldDetail': 'Notes',
'memFieldDate': 'Date',
'memFieldTime': 'Time',
'memFieldAmount': 'Amount',
'memFieldOwner': 'Owner',
'memFieldRepeat': 'Repeat',
'memFieldRemind': 'Remind me',
'memRemindOff': 'No reminder',
'memRemindMinutes': '@n minutes before',
'memTitleRequired': 'Content cannot be empty',
'memAmountInvalid': 'Invalid amount',
'memSaveFailed': 'Save failed, please try again',
'memDeleteFailed': 'Delete failed, please try again',
'memCompleteFailed': 'Action failed, please try again',
'memOffline': 'Showing locally cached data',
'memFromMeeting': 'From meeting',
'memDatePending': 'Date to be set',
'memDueRaw': 'Due: @raw',
'memSummaryTodo': '@n to-do',
'memSummaryIdea': '@n idea',
'memSummaryAlarm': '@n alarm',
'memSummaryExpense': 'Spent @amount',
'memReminderTitle': 'Reminder',
'memNotifyDisabled': 'Notifications are off; reminders will only show while the app is open',
'memGoPermission': 'Settings',
'memReportWeekTitle': 'Last week',
'memReportMonthTitle': 'Last month',
'memReportConfirm': 'Got it',
'memReportPlay': 'Play',
'memReportStop': 'Stop',
'memDailyTitle': 'Today\'s plan',
'memDailyPlay': 'Play',
'memDailyEmpty': 'Nothing planned today',
'memDailyClose': 'Close',
'tabTodo': 'Αναμνήσεις',
// 通用
'continue': 'Συνέχεια',
'confirm': 'Επιβεβαίωση',

100
apps/client/lib/core/translations/language/en_us.dart

@ -1,5 +1,59 @@
//美国-英语
const Map<String, String> enUS = {
'memDailyCount': 'You have @n item(s) today.',
'memCatAll': 'All',
'memCatTodo': 'To-do',
'memCatAlarm': 'Alarm',
'memCatIdea': 'Idea',
'memCatExpense': 'Expense',
'memCatOther': 'Other',
'memRepeatOnce': 'Once',
'memRepeatDaily': 'Daily',
'memRepeatWeekly': 'Weekly',
'memRepeatWeekdays': 'Weekdays',
'memRepeatWeekend': 'Weekends',
'memRepeatMonthly': 'Monthly',
'memEmptyTitle': 'Nothing recorded for this day',
'memEmptySubtitle': 'Tap + to add one, or ask EMAI to note it for you',
'memAddTitle': 'New',
'memEditTitle': 'Edit',
'memDelete': 'Delete',
'memDeleteConfirm': 'Delete this entry?',
'memFieldTitle': 'Content',
'memFieldDetail': 'Notes',
'memFieldDate': 'Date',
'memFieldTime': 'Time',
'memFieldAmount': 'Amount',
'memFieldOwner': 'Owner',
'memFieldRepeat': 'Repeat',
'memFieldRemind': 'Remind me',
'memRemindOff': 'No reminder',
'memRemindMinutes': '@n minutes before',
'memTitleRequired': 'Content cannot be empty',
'memAmountInvalid': 'Invalid amount',
'memSaveFailed': 'Save failed, please try again',
'memDeleteFailed': 'Delete failed, please try again',
'memCompleteFailed': 'Action failed, please try again',
'memOffline': 'Showing locally cached data',
'memFromMeeting': 'From meeting',
'memDatePending': 'Date to be set',
'memDueRaw': 'Due: @raw',
'memSummaryTodo': '@n to-do',
'memSummaryIdea': '@n idea',
'memSummaryAlarm': '@n alarm',
'memSummaryExpense': 'Spent @amount',
'memReminderTitle': 'Reminder',
'memNotifyDisabled': 'Notifications are off; reminders will only show while the app is open',
'memGoPermission': 'Settings',
'memReportWeekTitle': 'Last week',
'memReportMonthTitle': 'Last month',
'memReportConfirm': 'Got it',
'memReportPlay': 'Play',
'memReportStop': 'Stop',
'memDailyTitle': 'Today\'s plan',
'memDailyPlay': 'Play',
'memDailyEmpty': 'Nothing planned today',
'memDailyClose': 'Close',
// 通用
'continue': 'Continue',
'confirm': 'Confirm',
@ -1767,7 +1821,7 @@ const Map<String, String> enUS = {
// ==== 界面文案补齐(自启动授权 / 设备页 / AI页 / 广场 等)====
'tabDevice': 'Devices',
'tabTodo': 'Remin',
'tabTodo': 'Recall',
'tabPlaza': 'Plaza',
'sloganHome': 'Technology for a smarter life',
'sloganAi': 'Limitless intelligence',
@ -1917,50 +1971,6 @@ const Map<String, String> enUS = {
'recordPause': 'Pause',
'recordResume': 'Resume',
'recordDeleteConfirm': 'Delete this recording?',
'demoEvt1': 'Morning English practice',
'demoEvt2': 'Weekly project sync',
'demoEvt3': 'Drink water',
'demoEvt4': 'Submit weekly report',
'demoEvt5': 'Workout: chest & triceps',
'demoEvt6': 'Reading: Deep Work',
'demoEvt7': 'Dentist follow-up',
'demoEvt8': 'Breakfast',
'demoEvt9': 'Ride to work',
'demoEvt10': 'Top up phone bill',
'demoEvt11': 'Movie tickets',
'demoEvt12': 'Buy a book',
'demoDesc1': '20 minutes of shadowing, focus on phonetics.',
'demoDesc2': 'Review this week and plan the next.',
'demoDesc3': 'Every two hours.',
'demoDesc4': 'Sent to the team mailbox.',
'demoDesc5': '10 min warm-up, 40 min working sets.',
'demoDesc6': 'Finish chapter 3 and take notes.',
'demoDesc7': 'Bring the previous report.',
'demoDesc8': 'Soy milk and buns.',
'demoDesc9': 'Taxi at rush hour to avoid the crowded metro.',
'demoDesc10': 'Monthly auto top-up.',
'demoDesc11': 'Late show with friends.',
'demoDesc12': 'Print edition of Deep Work.',
'demoLoc1': 'Study',
'demoLoc2': 'Meeting room A',
'demoLoc4': 'Desk',
'demoLoc5': 'Gym',
'demoLoc7': 'Dental clinic',
'demoLoc8': 'Breakfast shop downstairs',
'demoLoc9': 'DiDi',
'demoLoc10': 'Carrier app',
'demoLoc11': 'Cinema',
'demoLoc12': 'Online bookstore',
'demoTagStudy': 'Study',
'demoTagMeeting': 'Meeting',
'demoTagReminder': 'Reminder',
'demoTagTask': 'Task',
'demoTagSport': 'Fitness',
'demoTagBooking': 'Appointment',
'demoTagFood': 'Food',
'demoTagTransit': 'Transport',
'demoTagBills': 'Bills',
'demoTagFun': 'Entertainment',
'scopeDay': 'Day',
'scopeWeek': 'Week',
'scopeMonth': 'Month',

100
apps/client/lib/core/translations/language/es_es.dart

@ -1,5 +1,59 @@
//西班牙-西班牙语
const Map<String, String> esES = {
'memDailyCount': 'You have @n item(s) today.',
'memCatAll': 'All',
'memCatTodo': 'To-do',
'memCatAlarm': 'Alarm',
'memCatIdea': 'Idea',
'memCatExpense': 'Expense',
'memCatOther': 'Other',
'memRepeatOnce': 'Once',
'memRepeatDaily': 'Daily',
'memRepeatWeekly': 'Weekly',
'memRepeatWeekdays': 'Weekdays',
'memRepeatWeekend': 'Weekends',
'memRepeatMonthly': 'Monthly',
'memEmptyTitle': 'Nothing recorded for this day',
'memEmptySubtitle': 'Tap + to add one, or ask EMAI to note it for you',
'memAddTitle': 'New',
'memEditTitle': 'Edit',
'memDelete': 'Delete',
'memDeleteConfirm': 'Delete this entry?',
'memFieldTitle': 'Content',
'memFieldDetail': 'Notes',
'memFieldDate': 'Date',
'memFieldTime': 'Time',
'memFieldAmount': 'Amount',
'memFieldOwner': 'Owner',
'memFieldRepeat': 'Repeat',
'memFieldRemind': 'Remind me',
'memRemindOff': 'No reminder',
'memRemindMinutes': '@n minutes before',
'memTitleRequired': 'Content cannot be empty',
'memAmountInvalid': 'Invalid amount',
'memSaveFailed': 'Save failed, please try again',
'memDeleteFailed': 'Delete failed, please try again',
'memCompleteFailed': 'Action failed, please try again',
'memOffline': 'Showing locally cached data',
'memFromMeeting': 'From meeting',
'memDatePending': 'Date to be set',
'memDueRaw': 'Due: @raw',
'memSummaryTodo': '@n to-do',
'memSummaryIdea': '@n idea',
'memSummaryAlarm': '@n alarm',
'memSummaryExpense': 'Spent @amount',
'memReminderTitle': 'Reminder',
'memNotifyDisabled': 'Notifications are off; reminders will only show while the app is open',
'memGoPermission': 'Settings',
'memReportWeekTitle': 'Last week',
'memReportMonthTitle': 'Last month',
'memReportConfirm': 'Got it',
'memReportPlay': 'Play',
'memReportStop': 'Stop',
'memDailyTitle': 'Today\'s plan',
'memDailyPlay': 'Play',
'memDailyEmpty': 'Nothing planned today',
'memDailyClose': 'Close',
// 通用
'continue': 'Continuar',
'confirm': 'Confirmar',
@ -1768,7 +1822,7 @@ const Map<String, String> esES = {
// ==== 界面文案补齐(自启动授权 / 设备页 / AI页 / 广场 等)====
'tabDevice': 'Dispositivos',
'tabTodo': 'Remin',
'tabTodo': 'Recuerdos',
'tabPlaza': 'Plaza',
'sloganHome': 'Tecnología para una vida inteligente',
'sloganAi': 'Inteligencia sin límites',
@ -1922,50 +1976,6 @@ const Map<String, String> esES = {
'recordPause': 'Pausar',
'recordResume': 'Reanudar',
'recordDeleteConfirm': '¿Eliminar esta grabación?',
'demoEvt1': 'Práctica de inglés matutina',
'demoEvt2': 'Reunión semanal del proyecto',
'demoEvt3': 'Recordatorio de beber agua',
'demoEvt4': 'Enviar informe semanal',
'demoEvt5': 'Entreno: pecho y tríceps',
'demoEvt6': 'Lectura: Deep Work',
'demoEvt7': 'Revisión dental',
'demoEvt8': 'Desayuno',
'demoEvt9': 'Taxi al trabajo',
'demoEvt10': 'Recarga de móvil',
'demoEvt11': 'Entradas de cine',
'demoEvt12': 'Comprar un libro',
'demoDesc1': '20 minutos de repetición, centrado en fonética.',
'demoDesc2': 'Repasar la semana y planificar la siguiente.',
'demoDesc3': 'Cada dos horas.',
'demoDesc4': 'Enviado al correo del equipo.',
'demoDesc5': '10 min de calentamiento, 40 min de series.',
'demoDesc6': 'Terminar el capítulo 3 y tomar notas.',
'demoDesc7': 'Llevar el informe anterior.',
'demoDesc8': 'Leche de soja y bollos.',
'demoDesc9': 'Taxi en hora punta para evitar el metro lleno.',
'demoDesc10': 'Recarga mensual automática.',
'demoDesc11': 'Sesión nocturna con amigos.',
'demoDesc12': 'Edición impresa de Deep Work.',
'demoLoc1': 'Estudio',
'demoLoc2': 'Sala A',
'demoLoc4': 'Escritorio',
'demoLoc5': 'Gimnasio',
'demoLoc7': 'Clínica dental',
'demoLoc8': 'Cafetería de abajo',
'demoLoc9': 'App de taxi',
'demoLoc10': 'App del operador',
'demoLoc11': 'Cine',
'demoLoc12': 'Librería online',
'demoTagStudy': 'Estudio',
'demoTagMeeting': 'Reunión',
'demoTagReminder': 'Recordatorio',
'demoTagTask': 'Tarea',
'demoTagSport': 'Deporte',
'demoTagBooking': 'Cita',
'demoTagFood': 'Comida',
'demoTagTransit': 'Transporte',
'demoTagBills': 'Facturas',
'demoTagFun': 'Ocio',
'scopeDay': 'Día',
'scopeWeek': 'Semana',
'scopeMonth': 'Mes',

55
apps/client/lib/core/translations/language/et_ee.dart

@ -1,5 +1,60 @@
//爱沙尼亚-爱沙尼亚语
const Map<String, String> etEE = {
'memDailyCount': 'You have @n item(s) today.',
'memCatAll': 'All',
'memCatTodo': 'To-do',
'memCatAlarm': 'Alarm',
'memCatIdea': 'Idea',
'memCatExpense': 'Expense',
'memCatOther': 'Other',
'memRepeatOnce': 'Once',
'memRepeatDaily': 'Daily',
'memRepeatWeekly': 'Weekly',
'memRepeatWeekdays': 'Weekdays',
'memRepeatWeekend': 'Weekends',
'memRepeatMonthly': 'Monthly',
'memEmptyTitle': 'Nothing recorded for this day',
'memEmptySubtitle': 'Tap + to add one, or ask EMAI to note it for you',
'memAddTitle': 'New',
'memEditTitle': 'Edit',
'memDelete': 'Delete',
'memDeleteConfirm': 'Delete this entry?',
'memFieldTitle': 'Content',
'memFieldDetail': 'Notes',
'memFieldDate': 'Date',
'memFieldTime': 'Time',
'memFieldAmount': 'Amount',
'memFieldOwner': 'Owner',
'memFieldRepeat': 'Repeat',
'memFieldRemind': 'Remind me',
'memRemindOff': 'No reminder',
'memRemindMinutes': '@n minutes before',
'memTitleRequired': 'Content cannot be empty',
'memAmountInvalid': 'Invalid amount',
'memSaveFailed': 'Save failed, please try again',
'memDeleteFailed': 'Delete failed, please try again',
'memCompleteFailed': 'Action failed, please try again',
'memOffline': 'Showing locally cached data',
'memFromMeeting': 'From meeting',
'memDatePending': 'Date to be set',
'memDueRaw': 'Due: @raw',
'memSummaryTodo': '@n to-do',
'memSummaryIdea': '@n idea',
'memSummaryAlarm': '@n alarm',
'memSummaryExpense': 'Spent @amount',
'memReminderTitle': 'Reminder',
'memNotifyDisabled': 'Notifications are off; reminders will only show while the app is open',
'memGoPermission': 'Settings',
'memReportWeekTitle': 'Last week',
'memReportMonthTitle': 'Last month',
'memReportConfirm': 'Got it',
'memReportPlay': 'Play',
'memReportStop': 'Stop',
'memDailyTitle': 'Today\'s plan',
'memDailyPlay': 'Play',
'memDailyEmpty': 'Nothing planned today',
'memDailyClose': 'Close',
'tabTodo': 'Mälestused',
// 通用
'continue': 'Jätka',
'confirm': 'Kinnita',

55
apps/client/lib/core/translations/language/fa_ir.dart

@ -1,5 +1,60 @@
//伊朗-波斯语
const Map<String, String> faIR = {
'memDailyCount': 'You have @n item(s) today.',
'memCatAll': 'All',
'memCatTodo': 'To-do',
'memCatAlarm': 'Alarm',
'memCatIdea': 'Idea',
'memCatExpense': 'Expense',
'memCatOther': 'Other',
'memRepeatOnce': 'Once',
'memRepeatDaily': 'Daily',
'memRepeatWeekly': 'Weekly',
'memRepeatWeekdays': 'Weekdays',
'memRepeatWeekend': 'Weekends',
'memRepeatMonthly': 'Monthly',
'memEmptyTitle': 'Nothing recorded for this day',
'memEmptySubtitle': 'Tap + to add one, or ask EMAI to note it for you',
'memAddTitle': 'New',
'memEditTitle': 'Edit',
'memDelete': 'Delete',
'memDeleteConfirm': 'Delete this entry?',
'memFieldTitle': 'Content',
'memFieldDetail': 'Notes',
'memFieldDate': 'Date',
'memFieldTime': 'Time',
'memFieldAmount': 'Amount',
'memFieldOwner': 'Owner',
'memFieldRepeat': 'Repeat',
'memFieldRemind': 'Remind me',
'memRemindOff': 'No reminder',
'memRemindMinutes': '@n minutes before',
'memTitleRequired': 'Content cannot be empty',
'memAmountInvalid': 'Invalid amount',
'memSaveFailed': 'Save failed, please try again',
'memDeleteFailed': 'Delete failed, please try again',
'memCompleteFailed': 'Action failed, please try again',
'memOffline': 'Showing locally cached data',
'memFromMeeting': 'From meeting',
'memDatePending': 'Date to be set',
'memDueRaw': 'Due: @raw',
'memSummaryTodo': '@n to-do',
'memSummaryIdea': '@n idea',
'memSummaryAlarm': '@n alarm',
'memSummaryExpense': 'Spent @amount',
'memReminderTitle': 'Reminder',
'memNotifyDisabled': 'Notifications are off; reminders will only show while the app is open',
'memGoPermission': 'Settings',
'memReportWeekTitle': 'Last week',
'memReportMonthTitle': 'Last month',
'memReportConfirm': 'Got it',
'memReportPlay': 'Play',
'memReportStop': 'Stop',
'memDailyTitle': 'Today\'s plan',
'memDailyPlay': 'Play',
'memDailyEmpty': 'Nothing planned today',
'memDailyClose': 'Close',
'tabTodo': 'یادها',
// 通用
'continue': 'ادامه',
'confirm': 'تأیید',

55
apps/client/lib/core/translations/language/fi_fi.dart

@ -1,5 +1,60 @@
//芬兰-芬兰语
const Map<String, String> fiFI = {
'memDailyCount': 'You have @n item(s) today.',
'memCatAll': 'All',
'memCatTodo': 'To-do',
'memCatAlarm': 'Alarm',
'memCatIdea': 'Idea',
'memCatExpense': 'Expense',
'memCatOther': 'Other',
'memRepeatOnce': 'Once',
'memRepeatDaily': 'Daily',
'memRepeatWeekly': 'Weekly',
'memRepeatWeekdays': 'Weekdays',
'memRepeatWeekend': 'Weekends',
'memRepeatMonthly': 'Monthly',
'memEmptyTitle': 'Nothing recorded for this day',
'memEmptySubtitle': 'Tap + to add one, or ask EMAI to note it for you',
'memAddTitle': 'New',
'memEditTitle': 'Edit',
'memDelete': 'Delete',
'memDeleteConfirm': 'Delete this entry?',
'memFieldTitle': 'Content',
'memFieldDetail': 'Notes',
'memFieldDate': 'Date',
'memFieldTime': 'Time',
'memFieldAmount': 'Amount',
'memFieldOwner': 'Owner',
'memFieldRepeat': 'Repeat',
'memFieldRemind': 'Remind me',
'memRemindOff': 'No reminder',
'memRemindMinutes': '@n minutes before',
'memTitleRequired': 'Content cannot be empty',
'memAmountInvalid': 'Invalid amount',
'memSaveFailed': 'Save failed, please try again',
'memDeleteFailed': 'Delete failed, please try again',
'memCompleteFailed': 'Action failed, please try again',
'memOffline': 'Showing locally cached data',
'memFromMeeting': 'From meeting',
'memDatePending': 'Date to be set',
'memDueRaw': 'Due: @raw',
'memSummaryTodo': '@n to-do',
'memSummaryIdea': '@n idea',
'memSummaryAlarm': '@n alarm',
'memSummaryExpense': 'Spent @amount',
'memReminderTitle': 'Reminder',
'memNotifyDisabled': 'Notifications are off; reminders will only show while the app is open',
'memGoPermission': 'Settings',
'memReportWeekTitle': 'Last week',
'memReportMonthTitle': 'Last month',
'memReportConfirm': 'Got it',
'memReportPlay': 'Play',
'memReportStop': 'Stop',
'memDailyTitle': 'Today\'s plan',
'memDailyPlay': 'Play',
'memDailyEmpty': 'Nothing planned today',
'memDailyClose': 'Close',
'tabTodo': 'Muistot',
// 通用
'continue': 'Jatka',
'confirm': 'Vahvista',

100
apps/client/lib/core/translations/language/fr_fr.dart

@ -1,5 +1,59 @@
// 法国-法语
const Map<String, String> frFR = {
'memDailyCount': 'You have @n item(s) today.',
'memCatAll': 'All',
'memCatTodo': 'To-do',
'memCatAlarm': 'Alarm',
'memCatIdea': 'Idea',
'memCatExpense': 'Expense',
'memCatOther': 'Other',
'memRepeatOnce': 'Once',
'memRepeatDaily': 'Daily',
'memRepeatWeekly': 'Weekly',
'memRepeatWeekdays': 'Weekdays',
'memRepeatWeekend': 'Weekends',
'memRepeatMonthly': 'Monthly',
'memEmptyTitle': 'Nothing recorded for this day',
'memEmptySubtitle': 'Tap + to add one, or ask EMAI to note it for you',
'memAddTitle': 'New',
'memEditTitle': 'Edit',
'memDelete': 'Delete',
'memDeleteConfirm': 'Delete this entry?',
'memFieldTitle': 'Content',
'memFieldDetail': 'Notes',
'memFieldDate': 'Date',
'memFieldTime': 'Time',
'memFieldAmount': 'Amount',
'memFieldOwner': 'Owner',
'memFieldRepeat': 'Repeat',
'memFieldRemind': 'Remind me',
'memRemindOff': 'No reminder',
'memRemindMinutes': '@n minutes before',
'memTitleRequired': 'Content cannot be empty',
'memAmountInvalid': 'Invalid amount',
'memSaveFailed': 'Save failed, please try again',
'memDeleteFailed': 'Delete failed, please try again',
'memCompleteFailed': 'Action failed, please try again',
'memOffline': 'Showing locally cached data',
'memFromMeeting': 'From meeting',
'memDatePending': 'Date to be set',
'memDueRaw': 'Due: @raw',
'memSummaryTodo': '@n to-do',
'memSummaryIdea': '@n idea',
'memSummaryAlarm': '@n alarm',
'memSummaryExpense': 'Spent @amount',
'memReminderTitle': 'Reminder',
'memNotifyDisabled': 'Notifications are off; reminders will only show while the app is open',
'memGoPermission': 'Settings',
'memReportWeekTitle': 'Last week',
'memReportMonthTitle': 'Last month',
'memReportConfirm': 'Got it',
'memReportPlay': 'Play',
'memReportStop': 'Stop',
'memDailyTitle': 'Today\'s plan',
'memDailyPlay': 'Play',
'memDailyEmpty': 'Nothing planned today',
'memDailyClose': 'Close',
// 通用
'continue': 'Continuer',
'confirm': 'Confirmer',
@ -1807,7 +1861,7 @@ const Map<String, String> frFR = {
'currentConnection': 'Connexion actuelle',
'binding': 'Association…',
'tabDevice': 'Appareils',
'tabTodo': 'Remin',
'tabTodo': 'Souvenirs',
'tabPlaza': 'Plaza',
'sloganHome': 'La technologie au service du quotidien',
'sloganAi': 'Une intelligence sans limites',
@ -1960,50 +2014,6 @@ const Map<String, String> frFR = {
'recordPause': 'Pause',
'recordResume': 'Reprendre',
'recordDeleteConfirm': 'Supprimer cet enregistrement ?',
'demoEvt1': 'Anglais du matin',
'demoEvt2': 'Point projet hebdo',
'demoEvt3': 'Boire de l’eau',
'demoEvt4': 'Envoyer le rapport hebdo',
'demoEvt5': 'Sport : pecs et triceps',
'demoEvt6': 'Lecture : Deep Work',
'demoEvt7': 'Contrôle dentaire',
'demoEvt8': 'Petit-déjeuner',
'demoEvt9': 'Taxi pour le travail',
'demoEvt10': 'Recharge mobile',
'demoEvt11': 'Billets de cinéma',
'demoEvt12': 'Acheter un livre',
'demoDesc1': '20 minutes de shadowing, focus sur la phonétique.',
'demoDesc2': 'Bilan de la semaine et planning suivant.',
'demoDesc3': 'Toutes les deux heures.',
'demoDesc4': 'Envoyé à la boîte de l’équipe.',
'demoDesc5': '10 min d’échauffement, 40 min de séries.',
'demoDesc6': 'Finir le chapitre 3 et prendre des notes.',
'demoDesc7': 'Penser au compte rendu précédent.',
'demoDesc8': 'Lait de soja et brioches.',
'demoDesc9': 'Taxi aux heures de pointe pour éviter le métro bondé.',
'demoDesc10': 'Recharge mensuelle automatique.',
'demoDesc11': 'Séance tardive avec des amis.',
'demoDesc12': 'Édition papier de Deep Work.',
'demoLoc1': 'Bureau',
'demoLoc2': 'Salle A',
'demoLoc4': 'Poste de travail',
'demoLoc5': 'Salle de sport',
'demoLoc7': 'Cabinet dentaire',
'demoLoc8': 'Café en bas',
'demoLoc9': 'Appli VTC',
'demoLoc10': 'Appli opérateur',
'demoLoc11': 'Cinéma',
'demoLoc12': 'Librairie en ligne',
'demoTagStudy': 'Étude',
'demoTagMeeting': 'Réunion',
'demoTagReminder': 'Rappel',
'demoTagTask': 'Tâche',
'demoTagSport': 'Sport',
'demoTagBooking': 'Rendez-vous',
'demoTagFood': 'Repas',
'demoTagTransit': 'Transport',
'demoTagBills': 'Factures',
'demoTagFun': 'Loisirs',
'scopeDay': 'Jour',
'scopeWeek': 'Semaine',
'scopeMonth': 'Mois',

55
apps/client/lib/core/translations/language/hi_in.dart

@ -1,5 +1,60 @@
// 印地-印地语
const Map<String, String> hiIN = {
'memDailyCount': 'You have @n item(s) today.',
'memCatAll': 'All',
'memCatTodo': 'To-do',
'memCatAlarm': 'Alarm',
'memCatIdea': 'Idea',
'memCatExpense': 'Expense',
'memCatOther': 'Other',
'memRepeatOnce': 'Once',
'memRepeatDaily': 'Daily',
'memRepeatWeekly': 'Weekly',
'memRepeatWeekdays': 'Weekdays',
'memRepeatWeekend': 'Weekends',
'memRepeatMonthly': 'Monthly',
'memEmptyTitle': 'Nothing recorded for this day',
'memEmptySubtitle': 'Tap + to add one, or ask EMAI to note it for you',
'memAddTitle': 'New',
'memEditTitle': 'Edit',
'memDelete': 'Delete',
'memDeleteConfirm': 'Delete this entry?',
'memFieldTitle': 'Content',
'memFieldDetail': 'Notes',
'memFieldDate': 'Date',
'memFieldTime': 'Time',
'memFieldAmount': 'Amount',
'memFieldOwner': 'Owner',
'memFieldRepeat': 'Repeat',
'memFieldRemind': 'Remind me',
'memRemindOff': 'No reminder',
'memRemindMinutes': '@n minutes before',
'memTitleRequired': 'Content cannot be empty',
'memAmountInvalid': 'Invalid amount',
'memSaveFailed': 'Save failed, please try again',
'memDeleteFailed': 'Delete failed, please try again',
'memCompleteFailed': 'Action failed, please try again',
'memOffline': 'Showing locally cached data',
'memFromMeeting': 'From meeting',
'memDatePending': 'Date to be set',
'memDueRaw': 'Due: @raw',
'memSummaryTodo': '@n to-do',
'memSummaryIdea': '@n idea',
'memSummaryAlarm': '@n alarm',
'memSummaryExpense': 'Spent @amount',
'memReminderTitle': 'Reminder',
'memNotifyDisabled': 'Notifications are off; reminders will only show while the app is open',
'memGoPermission': 'Settings',
'memReportWeekTitle': 'Last week',
'memReportMonthTitle': 'Last month',
'memReportConfirm': 'Got it',
'memReportPlay': 'Play',
'memReportStop': 'Stop',
'memDailyTitle': 'Today\'s plan',
'memDailyPlay': 'Play',
'memDailyEmpty': 'Nothing planned today',
'memDailyClose': 'Close',
'tabTodo': 'यादें',
// 通用
'continue': 'जारी रखें',
'confirm': 'पुष्टि करें',

55
apps/client/lib/core/translations/language/hr_hr.dart

@ -1,5 +1,60 @@
// 克罗地亚-克罗地亚语
const Map<String, String> hrHR = {
'memDailyCount': 'You have @n item(s) today.',
'memCatAll': 'All',
'memCatTodo': 'To-do',
'memCatAlarm': 'Alarm',
'memCatIdea': 'Idea',
'memCatExpense': 'Expense',
'memCatOther': 'Other',
'memRepeatOnce': 'Once',
'memRepeatDaily': 'Daily',
'memRepeatWeekly': 'Weekly',
'memRepeatWeekdays': 'Weekdays',
'memRepeatWeekend': 'Weekends',
'memRepeatMonthly': 'Monthly',
'memEmptyTitle': 'Nothing recorded for this day',
'memEmptySubtitle': 'Tap + to add one, or ask EMAI to note it for you',
'memAddTitle': 'New',
'memEditTitle': 'Edit',
'memDelete': 'Delete',
'memDeleteConfirm': 'Delete this entry?',
'memFieldTitle': 'Content',
'memFieldDetail': 'Notes',
'memFieldDate': 'Date',
'memFieldTime': 'Time',
'memFieldAmount': 'Amount',
'memFieldOwner': 'Owner',
'memFieldRepeat': 'Repeat',
'memFieldRemind': 'Remind me',
'memRemindOff': 'No reminder',
'memRemindMinutes': '@n minutes before',
'memTitleRequired': 'Content cannot be empty',
'memAmountInvalid': 'Invalid amount',
'memSaveFailed': 'Save failed, please try again',
'memDeleteFailed': 'Delete failed, please try again',
'memCompleteFailed': 'Action failed, please try again',
'memOffline': 'Showing locally cached data',
'memFromMeeting': 'From meeting',
'memDatePending': 'Date to be set',
'memDueRaw': 'Due: @raw',
'memSummaryTodo': '@n to-do',
'memSummaryIdea': '@n idea',
'memSummaryAlarm': '@n alarm',
'memSummaryExpense': 'Spent @amount',
'memReminderTitle': 'Reminder',
'memNotifyDisabled': 'Notifications are off; reminders will only show while the app is open',
'memGoPermission': 'Settings',
'memReportWeekTitle': 'Last week',
'memReportMonthTitle': 'Last month',
'memReportConfirm': 'Got it',
'memReportPlay': 'Play',
'memReportStop': 'Stop',
'memDailyTitle': 'Today\'s plan',
'memDailyPlay': 'Play',
'memDailyEmpty': 'Nothing planned today',
'memDailyClose': 'Close',
'tabTodo': 'Sjećanja',
// 通用
'continue': 'Nastavi',
'confirm': 'Potvrdi',

55
apps/client/lib/core/translations/language/hu_hu.dart

@ -1,5 +1,60 @@
//匈牙利-匈牙利语
const Map<String, String> huHU = {
'memDailyCount': 'You have @n item(s) today.',
'memCatAll': 'All',
'memCatTodo': 'To-do',
'memCatAlarm': 'Alarm',
'memCatIdea': 'Idea',
'memCatExpense': 'Expense',
'memCatOther': 'Other',
'memRepeatOnce': 'Once',
'memRepeatDaily': 'Daily',
'memRepeatWeekly': 'Weekly',
'memRepeatWeekdays': 'Weekdays',
'memRepeatWeekend': 'Weekends',
'memRepeatMonthly': 'Monthly',
'memEmptyTitle': 'Nothing recorded for this day',
'memEmptySubtitle': 'Tap + to add one, or ask EMAI to note it for you',
'memAddTitle': 'New',
'memEditTitle': 'Edit',
'memDelete': 'Delete',
'memDeleteConfirm': 'Delete this entry?',
'memFieldTitle': 'Content',
'memFieldDetail': 'Notes',
'memFieldDate': 'Date',
'memFieldTime': 'Time',
'memFieldAmount': 'Amount',
'memFieldOwner': 'Owner',
'memFieldRepeat': 'Repeat',
'memFieldRemind': 'Remind me',
'memRemindOff': 'No reminder',
'memRemindMinutes': '@n minutes before',
'memTitleRequired': 'Content cannot be empty',
'memAmountInvalid': 'Invalid amount',
'memSaveFailed': 'Save failed, please try again',
'memDeleteFailed': 'Delete failed, please try again',
'memCompleteFailed': 'Action failed, please try again',
'memOffline': 'Showing locally cached data',
'memFromMeeting': 'From meeting',
'memDatePending': 'Date to be set',
'memDueRaw': 'Due: @raw',
'memSummaryTodo': '@n to-do',
'memSummaryIdea': '@n idea',
'memSummaryAlarm': '@n alarm',
'memSummaryExpense': 'Spent @amount',
'memReminderTitle': 'Reminder',
'memNotifyDisabled': 'Notifications are off; reminders will only show while the app is open',
'memGoPermission': 'Settings',
'memReportWeekTitle': 'Last week',
'memReportMonthTitle': 'Last month',
'memReportConfirm': 'Got it',
'memReportPlay': 'Play',
'memReportStop': 'Stop',
'memDailyTitle': 'Today\'s plan',
'memDailyPlay': 'Play',
'memDailyEmpty': 'Nothing planned today',
'memDailyClose': 'Close',
'tabTodo': 'Emlékek',
// 通用
'continue': 'Folytatás',
'confirm': 'Megerősítés',

55
apps/client/lib/core/translations/language/id_id.dart

@ -1,5 +1,60 @@
//印尼-印尼语
const Map<String, String> idID = {
'memDailyCount': 'You have @n item(s) today.',
'memCatAll': 'All',
'memCatTodo': 'To-do',
'memCatAlarm': 'Alarm',
'memCatIdea': 'Idea',
'memCatExpense': 'Expense',
'memCatOther': 'Other',
'memRepeatOnce': 'Once',
'memRepeatDaily': 'Daily',
'memRepeatWeekly': 'Weekly',
'memRepeatWeekdays': 'Weekdays',
'memRepeatWeekend': 'Weekends',
'memRepeatMonthly': 'Monthly',
'memEmptyTitle': 'Nothing recorded for this day',
'memEmptySubtitle': 'Tap + to add one, or ask EMAI to note it for you',
'memAddTitle': 'New',
'memEditTitle': 'Edit',
'memDelete': 'Delete',
'memDeleteConfirm': 'Delete this entry?',
'memFieldTitle': 'Content',
'memFieldDetail': 'Notes',
'memFieldDate': 'Date',
'memFieldTime': 'Time',
'memFieldAmount': 'Amount',
'memFieldOwner': 'Owner',
'memFieldRepeat': 'Repeat',
'memFieldRemind': 'Remind me',
'memRemindOff': 'No reminder',
'memRemindMinutes': '@n minutes before',
'memTitleRequired': 'Content cannot be empty',
'memAmountInvalid': 'Invalid amount',
'memSaveFailed': 'Save failed, please try again',
'memDeleteFailed': 'Delete failed, please try again',
'memCompleteFailed': 'Action failed, please try again',
'memOffline': 'Showing locally cached data',
'memFromMeeting': 'From meeting',
'memDatePending': 'Date to be set',
'memDueRaw': 'Due: @raw',
'memSummaryTodo': '@n to-do',
'memSummaryIdea': '@n idea',
'memSummaryAlarm': '@n alarm',
'memSummaryExpense': 'Spent @amount',
'memReminderTitle': 'Reminder',
'memNotifyDisabled': 'Notifications are off; reminders will only show while the app is open',
'memGoPermission': 'Settings',
'memReportWeekTitle': 'Last week',
'memReportMonthTitle': 'Last month',
'memReportConfirm': 'Got it',
'memReportPlay': 'Play',
'memReportStop': 'Stop',
'memDailyTitle': 'Today\'s plan',
'memDailyPlay': 'Play',
'memDailyEmpty': 'Nothing planned today',
'memDailyClose': 'Close',
'tabTodo': 'Kenangan',
// 通用
'continue': 'Lanjutkan',
'confirm': 'Konfirmasi',

55
apps/client/lib/core/translations/language/is_is.dart

@ -1,5 +1,60 @@
// 冰岛-冰岛语
const Map<String, String> isIS = {
'memDailyCount': 'You have @n item(s) today.',
'memCatAll': 'All',
'memCatTodo': 'To-do',
'memCatAlarm': 'Alarm',
'memCatIdea': 'Idea',
'memCatExpense': 'Expense',
'memCatOther': 'Other',
'memRepeatOnce': 'Once',
'memRepeatDaily': 'Daily',
'memRepeatWeekly': 'Weekly',
'memRepeatWeekdays': 'Weekdays',
'memRepeatWeekend': 'Weekends',
'memRepeatMonthly': 'Monthly',
'memEmptyTitle': 'Nothing recorded for this day',
'memEmptySubtitle': 'Tap + to add one, or ask EMAI to note it for you',
'memAddTitle': 'New',
'memEditTitle': 'Edit',
'memDelete': 'Delete',
'memDeleteConfirm': 'Delete this entry?',
'memFieldTitle': 'Content',
'memFieldDetail': 'Notes',
'memFieldDate': 'Date',
'memFieldTime': 'Time',
'memFieldAmount': 'Amount',
'memFieldOwner': 'Owner',
'memFieldRepeat': 'Repeat',
'memFieldRemind': 'Remind me',
'memRemindOff': 'No reminder',
'memRemindMinutes': '@n minutes before',
'memTitleRequired': 'Content cannot be empty',
'memAmountInvalid': 'Invalid amount',
'memSaveFailed': 'Save failed, please try again',
'memDeleteFailed': 'Delete failed, please try again',
'memCompleteFailed': 'Action failed, please try again',
'memOffline': 'Showing locally cached data',
'memFromMeeting': 'From meeting',
'memDatePending': 'Date to be set',
'memDueRaw': 'Due: @raw',
'memSummaryTodo': '@n to-do',
'memSummaryIdea': '@n idea',
'memSummaryAlarm': '@n alarm',
'memSummaryExpense': 'Spent @amount',
'memReminderTitle': 'Reminder',
'memNotifyDisabled': 'Notifications are off; reminders will only show while the app is open',
'memGoPermission': 'Settings',
'memReportWeekTitle': 'Last week',
'memReportMonthTitle': 'Last month',
'memReportConfirm': 'Got it',
'memReportPlay': 'Play',
'memReportStop': 'Stop',
'memDailyTitle': 'Today\'s plan',
'memDailyPlay': 'Play',
'memDailyEmpty': 'Nothing planned today',
'memDailyClose': 'Close',
'tabTodo': 'Minningar',
// 通用
'continue': 'Halda áfram',
'confirm': 'Staðfesta',

55
apps/client/lib/core/translations/language/it_it.dart

@ -1,5 +1,60 @@
// 意大利-意大利语
const Map<String, String> itIT = {
'memDailyCount': 'You have @n item(s) today.',
'memCatAll': 'All',
'memCatTodo': 'To-do',
'memCatAlarm': 'Alarm',
'memCatIdea': 'Idea',
'memCatExpense': 'Expense',
'memCatOther': 'Other',
'memRepeatOnce': 'Once',
'memRepeatDaily': 'Daily',
'memRepeatWeekly': 'Weekly',
'memRepeatWeekdays': 'Weekdays',
'memRepeatWeekend': 'Weekends',
'memRepeatMonthly': 'Monthly',
'memEmptyTitle': 'Nothing recorded for this day',
'memEmptySubtitle': 'Tap + to add one, or ask EMAI to note it for you',
'memAddTitle': 'New',
'memEditTitle': 'Edit',
'memDelete': 'Delete',
'memDeleteConfirm': 'Delete this entry?',
'memFieldTitle': 'Content',
'memFieldDetail': 'Notes',
'memFieldDate': 'Date',
'memFieldTime': 'Time',
'memFieldAmount': 'Amount',
'memFieldOwner': 'Owner',
'memFieldRepeat': 'Repeat',
'memFieldRemind': 'Remind me',
'memRemindOff': 'No reminder',
'memRemindMinutes': '@n minutes before',
'memTitleRequired': 'Content cannot be empty',
'memAmountInvalid': 'Invalid amount',
'memSaveFailed': 'Save failed, please try again',
'memDeleteFailed': 'Delete failed, please try again',
'memCompleteFailed': 'Action failed, please try again',
'memOffline': 'Showing locally cached data',
'memFromMeeting': 'From meeting',
'memDatePending': 'Date to be set',
'memDueRaw': 'Due: @raw',
'memSummaryTodo': '@n to-do',
'memSummaryIdea': '@n idea',
'memSummaryAlarm': '@n alarm',
'memSummaryExpense': 'Spent @amount',
'memReminderTitle': 'Reminder',
'memNotifyDisabled': 'Notifications are off; reminders will only show while the app is open',
'memGoPermission': 'Settings',
'memReportWeekTitle': 'Last week',
'memReportMonthTitle': 'Last month',
'memReportConfirm': 'Got it',
'memReportPlay': 'Play',
'memReportStop': 'Stop',
'memDailyTitle': 'Today\'s plan',
'memDailyPlay': 'Play',
'memDailyEmpty': 'Nothing planned today',
'memDailyClose': 'Close',
'tabTodo': 'Ricordi',
// 通用
'continue': 'Continua',
'confirm': 'Conferma',

100
apps/client/lib/core/translations/language/ja_jp.dart

@ -1,5 +1,59 @@
// 日本-日本語
const Map<String, String> jaJP = {
'memDailyCount': 'You have @n item(s) today.',
'memCatAll': 'All',
'memCatTodo': 'To-do',
'memCatAlarm': 'Alarm',
'memCatIdea': 'Idea',
'memCatExpense': 'Expense',
'memCatOther': 'Other',
'memRepeatOnce': 'Once',
'memRepeatDaily': 'Daily',
'memRepeatWeekly': 'Weekly',
'memRepeatWeekdays': 'Weekdays',
'memRepeatWeekend': 'Weekends',
'memRepeatMonthly': 'Monthly',
'memEmptyTitle': 'Nothing recorded for this day',
'memEmptySubtitle': 'Tap + to add one, or ask EMAI to note it for you',
'memAddTitle': 'New',
'memEditTitle': 'Edit',
'memDelete': 'Delete',
'memDeleteConfirm': 'Delete this entry?',
'memFieldTitle': 'Content',
'memFieldDetail': 'Notes',
'memFieldDate': 'Date',
'memFieldTime': 'Time',
'memFieldAmount': 'Amount',
'memFieldOwner': 'Owner',
'memFieldRepeat': 'Repeat',
'memFieldRemind': 'Remind me',
'memRemindOff': 'No reminder',
'memRemindMinutes': '@n minutes before',
'memTitleRequired': 'Content cannot be empty',
'memAmountInvalid': 'Invalid amount',
'memSaveFailed': 'Save failed, please try again',
'memDeleteFailed': 'Delete failed, please try again',
'memCompleteFailed': 'Action failed, please try again',
'memOffline': 'Showing locally cached data',
'memFromMeeting': 'From meeting',
'memDatePending': 'Date to be set',
'memDueRaw': 'Due: @raw',
'memSummaryTodo': '@n to-do',
'memSummaryIdea': '@n idea',
'memSummaryAlarm': '@n alarm',
'memSummaryExpense': 'Spent @amount',
'memReminderTitle': 'Reminder',
'memNotifyDisabled': 'Notifications are off; reminders will only show while the app is open',
'memGoPermission': 'Settings',
'memReportWeekTitle': 'Last week',
'memReportMonthTitle': 'Last month',
'memReportConfirm': 'Got it',
'memReportPlay': 'Play',
'memReportStop': 'Stop',
'memDailyTitle': 'Today\'s plan',
'memDailyPlay': 'Play',
'memDailyEmpty': 'Nothing planned today',
'memDailyClose': 'Close',
// 通用
'continue': '続ける',
'confirm': '確認',
@ -1593,7 +1647,7 @@ const Map<String, String> jaJP = {
'meetingRecordScreenCapturePermissionDenied':
'システム音声を録音するには「画面収録」の許可が必要です。許可してから再試行してください',
'tabDevice': 'デバイス',
'tabTodo': 'Remin',
'tabTodo': 'ふりかえり',
'tabPlaza': 'プラザ',
'sloganHome': 'テクノロジーでスマートな毎日を',
'sloganAi': '無限の知性、無限の可能性',
@ -1735,50 +1789,6 @@ const Map<String, String> jaJP = {
'recordPause': '一時停止',
'recordResume': '再開',
'recordDeleteConfirm': 'この録音を削除しますか?',
'demoEvt1': '朝の英語シャドーイング',
'demoEvt2': 'プロジェクト定例',
'demoEvt3': '水分補給のリマインド',
'demoEvt4': '週報の提出',
'demoEvt5': 'トレーニング:胸・三頭',
'demoEvt6': '読書:ディープ・ワーク',
'demoEvt7': '歯科の再診',
'demoEvt8': '朝食',
'demoEvt9': 'タクシーで出勤',
'demoEvt10': '携帯料金のチャージ',
'demoEvt11': '映画のチケット',
'demoEvt12': '本を買う',
'demoDesc1': '20 分のシャドーイング、発音を重点的に。',
'demoDesc2': '今週の進捗と来週の予定を共有。',
'demoDesc3': '2 時間ごとに 1 回。',
'demoDesc4': 'チームのメールに送信済み。',
'demoDesc5': 'ウォームアップ 10 分、本番 40 分。',
'demoDesc6': '第 3 章を読み終えてメモを取る。',
'demoDesc7': '前回の検査票を忘れずに。',
'demoDesc8': '豆乳と肉まん。',
'demoDesc9': '混雑を避けて朝のラッシュはタクシーで。',
'demoDesc10': '毎月の自動チャージ。',
'demoDesc11': '友達とレイトショー。',
'demoDesc12': '『ディープ・ワーク』の紙版。',
'demoLoc1': '書斎',
'demoLoc2': '会議室 A',
'demoLoc4': '自席',
'demoLoc5': 'ジム',
'demoLoc7': '歯科医院',
'demoLoc8': '階下の朝食店',
'demoLoc9': '配車アプリ',
'demoLoc10': 'キャリアアプリ',
'demoLoc11': '映画館',
'demoLoc12': 'オンライン書店',
'demoTagStudy': '学習',
'demoTagMeeting': '会議',
'demoTagReminder': 'リマインド',
'demoTagTask': 'タスク',
'demoTagSport': '運動',
'demoTagBooking': '予約',
'demoTagFood': '食事',
'demoTagTransit': '交通',
'demoTagBills': '支払い',
'demoTagFun': '娯楽',
'scopeDay': '日',
'scopeWeek': '週',
'scopeMonth': '月',

100
apps/client/lib/core/translations/language/ko_kr.dart

@ -1,5 +1,59 @@
// 韩国-韩语
const Map<String, String> koKR = {
'memDailyCount': 'You have @n item(s) today.',
'memCatAll': 'All',
'memCatTodo': 'To-do',
'memCatAlarm': 'Alarm',
'memCatIdea': 'Idea',
'memCatExpense': 'Expense',
'memCatOther': 'Other',
'memRepeatOnce': 'Once',
'memRepeatDaily': 'Daily',
'memRepeatWeekly': 'Weekly',
'memRepeatWeekdays': 'Weekdays',
'memRepeatWeekend': 'Weekends',
'memRepeatMonthly': 'Monthly',
'memEmptyTitle': 'Nothing recorded for this day',
'memEmptySubtitle': 'Tap + to add one, or ask EMAI to note it for you',
'memAddTitle': 'New',
'memEditTitle': 'Edit',
'memDelete': 'Delete',
'memDeleteConfirm': 'Delete this entry?',
'memFieldTitle': 'Content',
'memFieldDetail': 'Notes',
'memFieldDate': 'Date',
'memFieldTime': 'Time',
'memFieldAmount': 'Amount',
'memFieldOwner': 'Owner',
'memFieldRepeat': 'Repeat',
'memFieldRemind': 'Remind me',
'memRemindOff': 'No reminder',
'memRemindMinutes': '@n minutes before',
'memTitleRequired': 'Content cannot be empty',
'memAmountInvalid': 'Invalid amount',
'memSaveFailed': 'Save failed, please try again',
'memDeleteFailed': 'Delete failed, please try again',
'memCompleteFailed': 'Action failed, please try again',
'memOffline': 'Showing locally cached data',
'memFromMeeting': 'From meeting',
'memDatePending': 'Date to be set',
'memDueRaw': 'Due: @raw',
'memSummaryTodo': '@n to-do',
'memSummaryIdea': '@n idea',
'memSummaryAlarm': '@n alarm',
'memSummaryExpense': 'Spent @amount',
'memReminderTitle': 'Reminder',
'memNotifyDisabled': 'Notifications are off; reminders will only show while the app is open',
'memGoPermission': 'Settings',
'memReportWeekTitle': 'Last week',
'memReportMonthTitle': 'Last month',
'memReportConfirm': 'Got it',
'memReportPlay': 'Play',
'memReportStop': 'Stop',
'memDailyTitle': 'Today\'s plan',
'memDailyPlay': 'Play',
'memDailyEmpty': 'Nothing planned today',
'memDailyClose': 'Close',
// 通用
'continue': '계속',
'confirm': '확인',
@ -1598,7 +1652,7 @@ const Map<String, String> koKR = {
'currentConnection': '현재 연결',
'binding': '등록 중…',
'tabDevice': '기기',
'tabTodo': 'Remin',
'tabTodo': '되새김',
'tabPlaza': '플라자',
'sloganHome': '기술로 더 스마트한 일상을',
'sloganAi': '무한한 지능, 무한한 가능성',
@ -1741,50 +1795,6 @@ const Map<String, String> koKR = {
'recordPause': '일시정지',
'recordResume': '계속',
'recordDeleteConfirm': '이 녹음을 삭제할까요?',
'demoEvt1': '아침 영어 섀도잉',
'demoEvt2': '프로젝트 주간 회의',
'demoEvt3': '물 마시기 알림',
'demoEvt4': '주간 보고 제출',
'demoEvt5': '운동: 가슴 + 삼두',
'demoEvt6': '독서: 딥 워크',
'demoEvt7': '치과 재진',
'demoEvt8': '아침 식사',
'demoEvt9': '택시로 출근',
'demoEvt10': '통신비 충전',
'demoEvt11': '영화 티켓',
'demoEvt12': '책 구매',
'demoDesc1': '20분 섀도잉, 발음 교정 위주.',
'demoDesc2': '이번 주 진행 상황과 다음 주 일정 공유.',
'demoDesc3': '두 시간마다 한 번.',
'demoDesc4': '팀 메일로 발송 완료.',
'demoDesc5': '워밍업 10분, 본 운동 40분.',
'demoDesc6': '3장을 끝내고 메모하기.',
'demoDesc7': '지난번 검사지 챙기기.',
'demoDesc8': '두유와 만두.',
'demoDesc9': '출근 혼잡을 피해 택시 이용.',
'demoDesc10': '매월 자동 충전.',
'demoDesc11': '친구와 심야 영화.',
'demoDesc12': '『딥 워크』 종이책.',
'demoLoc1': '서재',
'demoLoc2': '회의실 A',
'demoLoc4': '자리',
'demoLoc5': '헬스장',
'demoLoc7': '치과',
'demoLoc8': '아래층 아침 식당',
'demoLoc9': '호출 앱',
'demoLoc10': '통신사 앱',
'demoLoc11': '영화관',
'demoLoc12': '온라인 서점',
'demoTagStudy': '학습',
'demoTagMeeting': '회의',
'demoTagReminder': '알림',
'demoTagTask': '업무',
'demoTagSport': '운동',
'demoTagBooking': '예약',
'demoTagFood': '식사',
'demoTagTransit': '교통',
'demoTagBills': '납부',
'demoTagFun': '여가',
'scopeDay': '일',
'scopeWeek': '주',
'scopeMonth': '월',

55
apps/client/lib/core/translations/language/lt_lt.dart

@ -1,4 +1,59 @@
const Map<String, String> ltLT = {
'memDailyCount': 'You have @n item(s) today.',
'memCatAll': 'All',
'memCatTodo': 'To-do',
'memCatAlarm': 'Alarm',
'memCatIdea': 'Idea',
'memCatExpense': 'Expense',
'memCatOther': 'Other',
'memRepeatOnce': 'Once',
'memRepeatDaily': 'Daily',
'memRepeatWeekly': 'Weekly',
'memRepeatWeekdays': 'Weekdays',
'memRepeatWeekend': 'Weekends',
'memRepeatMonthly': 'Monthly',
'memEmptyTitle': 'Nothing recorded for this day',
'memEmptySubtitle': 'Tap + to add one, or ask EMAI to note it for you',
'memAddTitle': 'New',
'memEditTitle': 'Edit',
'memDelete': 'Delete',
'memDeleteConfirm': 'Delete this entry?',
'memFieldTitle': 'Content',
'memFieldDetail': 'Notes',
'memFieldDate': 'Date',
'memFieldTime': 'Time',
'memFieldAmount': 'Amount',
'memFieldOwner': 'Owner',
'memFieldRepeat': 'Repeat',
'memFieldRemind': 'Remind me',
'memRemindOff': 'No reminder',
'memRemindMinutes': '@n minutes before',
'memTitleRequired': 'Content cannot be empty',
'memAmountInvalid': 'Invalid amount',
'memSaveFailed': 'Save failed, please try again',
'memDeleteFailed': 'Delete failed, please try again',
'memCompleteFailed': 'Action failed, please try again',
'memOffline': 'Showing locally cached data',
'memFromMeeting': 'From meeting',
'memDatePending': 'Date to be set',
'memDueRaw': 'Due: @raw',
'memSummaryTodo': '@n to-do',
'memSummaryIdea': '@n idea',
'memSummaryAlarm': '@n alarm',
'memSummaryExpense': 'Spent @amount',
'memReminderTitle': 'Reminder',
'memNotifyDisabled': 'Notifications are off; reminders will only show while the app is open',
'memGoPermission': 'Settings',
'memReportWeekTitle': 'Last week',
'memReportMonthTitle': 'Last month',
'memReportConfirm': 'Got it',
'memReportPlay': 'Play',
'memReportStop': 'Stop',
'memDailyTitle': 'Today\'s plan',
'memDailyPlay': 'Play',
'memDailyEmpty': 'Nothing planned today',
'memDailyClose': 'Close',
'tabTodo': 'Prisiminimai',
// 通用
'continue': 'Tęsti',
'confirm': 'Patvirtinti',

55
apps/client/lib/core/translations/language/lv_lv.dart

@ -1,4 +1,59 @@
const Map<String, String> lvLV = {
'memDailyCount': 'You have @n item(s) today.',
'memCatAll': 'All',
'memCatTodo': 'To-do',
'memCatAlarm': 'Alarm',
'memCatIdea': 'Idea',
'memCatExpense': 'Expense',
'memCatOther': 'Other',
'memRepeatOnce': 'Once',
'memRepeatDaily': 'Daily',
'memRepeatWeekly': 'Weekly',
'memRepeatWeekdays': 'Weekdays',
'memRepeatWeekend': 'Weekends',
'memRepeatMonthly': 'Monthly',
'memEmptyTitle': 'Nothing recorded for this day',
'memEmptySubtitle': 'Tap + to add one, or ask EMAI to note it for you',
'memAddTitle': 'New',
'memEditTitle': 'Edit',
'memDelete': 'Delete',
'memDeleteConfirm': 'Delete this entry?',
'memFieldTitle': 'Content',
'memFieldDetail': 'Notes',
'memFieldDate': 'Date',
'memFieldTime': 'Time',
'memFieldAmount': 'Amount',
'memFieldOwner': 'Owner',
'memFieldRepeat': 'Repeat',
'memFieldRemind': 'Remind me',
'memRemindOff': 'No reminder',
'memRemindMinutes': '@n minutes before',
'memTitleRequired': 'Content cannot be empty',
'memAmountInvalid': 'Invalid amount',
'memSaveFailed': 'Save failed, please try again',
'memDeleteFailed': 'Delete failed, please try again',
'memCompleteFailed': 'Action failed, please try again',
'memOffline': 'Showing locally cached data',
'memFromMeeting': 'From meeting',
'memDatePending': 'Date to be set',
'memDueRaw': 'Due: @raw',
'memSummaryTodo': '@n to-do',
'memSummaryIdea': '@n idea',
'memSummaryAlarm': '@n alarm',
'memSummaryExpense': 'Spent @amount',
'memReminderTitle': 'Reminder',
'memNotifyDisabled': 'Notifications are off; reminders will only show while the app is open',
'memGoPermission': 'Settings',
'memReportWeekTitle': 'Last week',
'memReportMonthTitle': 'Last month',
'memReportConfirm': 'Got it',
'memReportPlay': 'Play',
'memReportStop': 'Stop',
'memDailyTitle': 'Today\'s plan',
'memDailyPlay': 'Play',
'memDailyEmpty': 'Nothing planned today',
'memDailyClose': 'Close',
'tabTodo': 'Atmiņas',
// 通用
'continue': 'Turpināt',
'confirm': 'Apstiprināt',

55
apps/client/lib/core/translations/language/ms_my.dart

@ -1,5 +1,60 @@
// 马来西亚-马来语
const Map<String, String> msMY = {
'memDailyCount': 'You have @n item(s) today.',
'memCatAll': 'All',
'memCatTodo': 'To-do',
'memCatAlarm': 'Alarm',
'memCatIdea': 'Idea',
'memCatExpense': 'Expense',
'memCatOther': 'Other',
'memRepeatOnce': 'Once',
'memRepeatDaily': 'Daily',
'memRepeatWeekly': 'Weekly',
'memRepeatWeekdays': 'Weekdays',
'memRepeatWeekend': 'Weekends',
'memRepeatMonthly': 'Monthly',
'memEmptyTitle': 'Nothing recorded for this day',
'memEmptySubtitle': 'Tap + to add one, or ask EMAI to note it for you',
'memAddTitle': 'New',
'memEditTitle': 'Edit',
'memDelete': 'Delete',
'memDeleteConfirm': 'Delete this entry?',
'memFieldTitle': 'Content',
'memFieldDetail': 'Notes',
'memFieldDate': 'Date',
'memFieldTime': 'Time',
'memFieldAmount': 'Amount',
'memFieldOwner': 'Owner',
'memFieldRepeat': 'Repeat',
'memFieldRemind': 'Remind me',
'memRemindOff': 'No reminder',
'memRemindMinutes': '@n minutes before',
'memTitleRequired': 'Content cannot be empty',
'memAmountInvalid': 'Invalid amount',
'memSaveFailed': 'Save failed, please try again',
'memDeleteFailed': 'Delete failed, please try again',
'memCompleteFailed': 'Action failed, please try again',
'memOffline': 'Showing locally cached data',
'memFromMeeting': 'From meeting',
'memDatePending': 'Date to be set',
'memDueRaw': 'Due: @raw',
'memSummaryTodo': '@n to-do',
'memSummaryIdea': '@n idea',
'memSummaryAlarm': '@n alarm',
'memSummaryExpense': 'Spent @amount',
'memReminderTitle': 'Reminder',
'memNotifyDisabled': 'Notifications are off; reminders will only show while the app is open',
'memGoPermission': 'Settings',
'memReportWeekTitle': 'Last week',
'memReportMonthTitle': 'Last month',
'memReportConfirm': 'Got it',
'memReportPlay': 'Play',
'memReportStop': 'Stop',
'memDailyTitle': 'Today\'s plan',
'memDailyPlay': 'Play',
'memDailyEmpty': 'Nothing planned today',
'memDailyClose': 'Close',
'tabTodo': 'Kenangan',
// 通用
'continue': 'Teruskan',
'confirm': 'Sahkan',

55
apps/client/lib/core/translations/language/nb_no.dart

@ -1,5 +1,60 @@
// 挪威-挪威语
const Map<String, String> nbNO = {
'memDailyCount': 'You have @n item(s) today.',
'memCatAll': 'All',
'memCatTodo': 'To-do',
'memCatAlarm': 'Alarm',
'memCatIdea': 'Idea',
'memCatExpense': 'Expense',
'memCatOther': 'Other',
'memRepeatOnce': 'Once',
'memRepeatDaily': 'Daily',
'memRepeatWeekly': 'Weekly',
'memRepeatWeekdays': 'Weekdays',
'memRepeatWeekend': 'Weekends',
'memRepeatMonthly': 'Monthly',
'memEmptyTitle': 'Nothing recorded for this day',
'memEmptySubtitle': 'Tap + to add one, or ask EMAI to note it for you',
'memAddTitle': 'New',
'memEditTitle': 'Edit',
'memDelete': 'Delete',
'memDeleteConfirm': 'Delete this entry?',
'memFieldTitle': 'Content',
'memFieldDetail': 'Notes',
'memFieldDate': 'Date',
'memFieldTime': 'Time',
'memFieldAmount': 'Amount',
'memFieldOwner': 'Owner',
'memFieldRepeat': 'Repeat',
'memFieldRemind': 'Remind me',
'memRemindOff': 'No reminder',
'memRemindMinutes': '@n minutes before',
'memTitleRequired': 'Content cannot be empty',
'memAmountInvalid': 'Invalid amount',
'memSaveFailed': 'Save failed, please try again',
'memDeleteFailed': 'Delete failed, please try again',
'memCompleteFailed': 'Action failed, please try again',
'memOffline': 'Showing locally cached data',
'memFromMeeting': 'From meeting',
'memDatePending': 'Date to be set',
'memDueRaw': 'Due: @raw',
'memSummaryTodo': '@n to-do',
'memSummaryIdea': '@n idea',
'memSummaryAlarm': '@n alarm',
'memSummaryExpense': 'Spent @amount',
'memReminderTitle': 'Reminder',
'memNotifyDisabled': 'Notifications are off; reminders will only show while the app is open',
'memGoPermission': 'Settings',
'memReportWeekTitle': 'Last week',
'memReportMonthTitle': 'Last month',
'memReportConfirm': 'Got it',
'memReportPlay': 'Play',
'memReportStop': 'Stop',
'memDailyTitle': 'Today\'s plan',
'memDailyPlay': 'Play',
'memDailyEmpty': 'Nothing planned today',
'memDailyClose': 'Close',
'tabTodo': 'Minner',
// 通用
'continue': 'Fortsett',
'confirm': 'Bekreft',

55
apps/client/lib/core/translations/language/nl_nl.dart

@ -1,5 +1,60 @@
// 荷兰-荷兰语
const Map<String, String> nlNL = {
'memDailyCount': 'You have @n item(s) today.',
'memCatAll': 'All',
'memCatTodo': 'To-do',
'memCatAlarm': 'Alarm',
'memCatIdea': 'Idea',
'memCatExpense': 'Expense',
'memCatOther': 'Other',
'memRepeatOnce': 'Once',
'memRepeatDaily': 'Daily',
'memRepeatWeekly': 'Weekly',
'memRepeatWeekdays': 'Weekdays',
'memRepeatWeekend': 'Weekends',
'memRepeatMonthly': 'Monthly',
'memEmptyTitle': 'Nothing recorded for this day',
'memEmptySubtitle': 'Tap + to add one, or ask EMAI to note it for you',
'memAddTitle': 'New',
'memEditTitle': 'Edit',
'memDelete': 'Delete',
'memDeleteConfirm': 'Delete this entry?',
'memFieldTitle': 'Content',
'memFieldDetail': 'Notes',
'memFieldDate': 'Date',
'memFieldTime': 'Time',
'memFieldAmount': 'Amount',
'memFieldOwner': 'Owner',
'memFieldRepeat': 'Repeat',
'memFieldRemind': 'Remind me',
'memRemindOff': 'No reminder',
'memRemindMinutes': '@n minutes before',
'memTitleRequired': 'Content cannot be empty',
'memAmountInvalid': 'Invalid amount',
'memSaveFailed': 'Save failed, please try again',
'memDeleteFailed': 'Delete failed, please try again',
'memCompleteFailed': 'Action failed, please try again',
'memOffline': 'Showing locally cached data',
'memFromMeeting': 'From meeting',
'memDatePending': 'Date to be set',
'memDueRaw': 'Due: @raw',
'memSummaryTodo': '@n to-do',
'memSummaryIdea': '@n idea',
'memSummaryAlarm': '@n alarm',
'memSummaryExpense': 'Spent @amount',
'memReminderTitle': 'Reminder',
'memNotifyDisabled': 'Notifications are off; reminders will only show while the app is open',
'memGoPermission': 'Settings',
'memReportWeekTitle': 'Last week',
'memReportMonthTitle': 'Last month',
'memReportConfirm': 'Got it',
'memReportPlay': 'Play',
'memReportStop': 'Stop',
'memDailyTitle': 'Today\'s plan',
'memDailyPlay': 'Play',
'memDailyEmpty': 'Nothing planned today',
'memDailyClose': 'Close',
'tabTodo': 'Herinneringen',
// 通用
'continue': 'Doorgaan',
'confirm': 'Bevestigen',

55
apps/client/lib/core/translations/language/pl_pl.dart

@ -1,5 +1,60 @@
// 波兰-波兰语
const Map<String, String> plPL = {
'memDailyCount': 'You have @n item(s) today.',
'memCatAll': 'All',
'memCatTodo': 'To-do',
'memCatAlarm': 'Alarm',
'memCatIdea': 'Idea',
'memCatExpense': 'Expense',
'memCatOther': 'Other',
'memRepeatOnce': 'Once',
'memRepeatDaily': 'Daily',
'memRepeatWeekly': 'Weekly',
'memRepeatWeekdays': 'Weekdays',
'memRepeatWeekend': 'Weekends',
'memRepeatMonthly': 'Monthly',
'memEmptyTitle': 'Nothing recorded for this day',
'memEmptySubtitle': 'Tap + to add one, or ask EMAI to note it for you',
'memAddTitle': 'New',
'memEditTitle': 'Edit',
'memDelete': 'Delete',
'memDeleteConfirm': 'Delete this entry?',
'memFieldTitle': 'Content',
'memFieldDetail': 'Notes',
'memFieldDate': 'Date',
'memFieldTime': 'Time',
'memFieldAmount': 'Amount',
'memFieldOwner': 'Owner',
'memFieldRepeat': 'Repeat',
'memFieldRemind': 'Remind me',
'memRemindOff': 'No reminder',
'memRemindMinutes': '@n minutes before',
'memTitleRequired': 'Content cannot be empty',
'memAmountInvalid': 'Invalid amount',
'memSaveFailed': 'Save failed, please try again',
'memDeleteFailed': 'Delete failed, please try again',
'memCompleteFailed': 'Action failed, please try again',
'memOffline': 'Showing locally cached data',
'memFromMeeting': 'From meeting',
'memDatePending': 'Date to be set',
'memDueRaw': 'Due: @raw',
'memSummaryTodo': '@n to-do',
'memSummaryIdea': '@n idea',
'memSummaryAlarm': '@n alarm',
'memSummaryExpense': 'Spent @amount',
'memReminderTitle': 'Reminder',
'memNotifyDisabled': 'Notifications are off; reminders will only show while the app is open',
'memGoPermission': 'Settings',
'memReportWeekTitle': 'Last week',
'memReportMonthTitle': 'Last month',
'memReportConfirm': 'Got it',
'memReportPlay': 'Play',
'memReportStop': 'Stop',
'memDailyTitle': 'Today\'s plan',
'memDailyPlay': 'Play',
'memDailyEmpty': 'Nothing planned today',
'memDailyClose': 'Close',
'tabTodo': 'Wspomnienia',
// 通用
'continue': 'Kontynuuj',
'confirm': 'Potwierdź',

100
apps/client/lib/core/translations/language/pt_pt.dart

@ -1,5 +1,59 @@
// 葡萄牙-葡萄牙语
const Map<String, String> ptPT = {
'memDailyCount': 'You have @n item(s) today.',
'memCatAll': 'All',
'memCatTodo': 'To-do',
'memCatAlarm': 'Alarm',
'memCatIdea': 'Idea',
'memCatExpense': 'Expense',
'memCatOther': 'Other',
'memRepeatOnce': 'Once',
'memRepeatDaily': 'Daily',
'memRepeatWeekly': 'Weekly',
'memRepeatWeekdays': 'Weekdays',
'memRepeatWeekend': 'Weekends',
'memRepeatMonthly': 'Monthly',
'memEmptyTitle': 'Nothing recorded for this day',
'memEmptySubtitle': 'Tap + to add one, or ask EMAI to note it for you',
'memAddTitle': 'New',
'memEditTitle': 'Edit',
'memDelete': 'Delete',
'memDeleteConfirm': 'Delete this entry?',
'memFieldTitle': 'Content',
'memFieldDetail': 'Notes',
'memFieldDate': 'Date',
'memFieldTime': 'Time',
'memFieldAmount': 'Amount',
'memFieldOwner': 'Owner',
'memFieldRepeat': 'Repeat',
'memFieldRemind': 'Remind me',
'memRemindOff': 'No reminder',
'memRemindMinutes': '@n minutes before',
'memTitleRequired': 'Content cannot be empty',
'memAmountInvalid': 'Invalid amount',
'memSaveFailed': 'Save failed, please try again',
'memDeleteFailed': 'Delete failed, please try again',
'memCompleteFailed': 'Action failed, please try again',
'memOffline': 'Showing locally cached data',
'memFromMeeting': 'From meeting',
'memDatePending': 'Date to be set',
'memDueRaw': 'Due: @raw',
'memSummaryTodo': '@n to-do',
'memSummaryIdea': '@n idea',
'memSummaryAlarm': '@n alarm',
'memSummaryExpense': 'Spent @amount',
'memReminderTitle': 'Reminder',
'memNotifyDisabled': 'Notifications are off; reminders will only show while the app is open',
'memGoPermission': 'Settings',
'memReportWeekTitle': 'Last week',
'memReportMonthTitle': 'Last month',
'memReportConfirm': 'Got it',
'memReportPlay': 'Play',
'memReportStop': 'Stop',
'memDailyTitle': 'Today\'s plan',
'memDailyPlay': 'Play',
'memDailyEmpty': 'Nothing planned today',
'memDailyClose': 'Close',
// 通用
'continue': 'Continuar',
'confirm': 'Confirmar',
@ -1794,7 +1848,7 @@ const Map<String, String> ptPT = {
'currentConnection': 'Ligação atual',
'binding': 'A associar…',
'tabDevice': 'Dispositivos',
'tabTodo': 'Remin',
'tabTodo': 'Memórias',
'tabPlaza': 'Plaza',
'sloganHome': 'Tecnologia para um dia a dia inteligente',
'sloganAi': 'Inteligência sem limites',
@ -1948,50 +2002,6 @@ const Map<String, String> ptPT = {
'recordPause': 'Pausa',
'recordResume': 'Retomar',
'recordDeleteConfirm': 'Eliminar esta gravação?',
'demoEvt1': 'Inglês da manhã',
'demoEvt2': 'Reunião semanal do projeto',
'demoEvt3': 'Beber água',
'demoEvt4': 'Entregar o relatório semanal',
'demoEvt5': 'Treino: peito e tríceps',
'demoEvt6': 'Leitura: Deep Work',
'demoEvt7': 'Consulta de dentista',
'demoEvt8': 'Pequeno-almoço',
'demoEvt9': 'Táxi para o trabalho',
'demoEvt10': 'Carregar o telemóvel',
'demoEvt11': 'Bilhetes de cinema',
'demoEvt12': 'Comprar um livro',
'demoDesc1': '20 minutos de shadowing, foco na fonética.',
'demoDesc2': 'Balanço da semana e plano da próxima.',
'demoDesc3': 'De duas em duas horas.',
'demoDesc4': 'Enviado para o email da equipa.',
'demoDesc5': '10 min de aquecimento, 40 min de séries.',
'demoDesc6': 'Terminar o capítulo 3 e tirar notas.',
'demoDesc7': 'Levar o relatório anterior.',
'demoDesc8': 'Leite de soja e pãezinhos.',
'demoDesc9': 'Táxi na hora de ponta para evitar o metro cheio.',
'demoDesc10': 'Carregamento automático mensal.',
'demoDesc11': 'Sessão tardia com amigos.',
'demoDesc12': 'Edição impressa de Deep Work.',
'demoLoc1': 'Escritório',
'demoLoc2': 'Sala A',
'demoLoc4': 'Secretária',
'demoLoc5': 'Ginásio',
'demoLoc7': 'Clínica dentária',
'demoLoc8': 'Café do rés do chão',
'demoLoc9': 'App de táxis',
'demoLoc10': 'App da operadora',
'demoLoc11': 'Cinema',
'demoLoc12': 'Livraria online',
'demoTagStudy': 'Estudo',
'demoTagMeeting': 'Reunião',
'demoTagReminder': 'Lembrete',
'demoTagTask': 'Tarefa',
'demoTagSport': 'Desporto',
'demoTagBooking': 'Marcação',
'demoTagFood': 'Refeição',
'demoTagTransit': 'Transporte',
'demoTagBills': 'Pagamentos',
'demoTagFun': 'Lazer',
'scopeDay': 'Dia',
'scopeWeek': 'Semana',
'scopeMonth': 'Mês',

55
apps/client/lib/core/translations/language/ro_ro.dart

@ -1,5 +1,60 @@
// 罗马尼亚-罗马尼亚语
const Map<String, String> roRO = {
'memDailyCount': 'You have @n item(s) today.',
'memCatAll': 'All',
'memCatTodo': 'To-do',
'memCatAlarm': 'Alarm',
'memCatIdea': 'Idea',
'memCatExpense': 'Expense',
'memCatOther': 'Other',
'memRepeatOnce': 'Once',
'memRepeatDaily': 'Daily',
'memRepeatWeekly': 'Weekly',
'memRepeatWeekdays': 'Weekdays',
'memRepeatWeekend': 'Weekends',
'memRepeatMonthly': 'Monthly',
'memEmptyTitle': 'Nothing recorded for this day',
'memEmptySubtitle': 'Tap + to add one, or ask EMAI to note it for you',
'memAddTitle': 'New',
'memEditTitle': 'Edit',
'memDelete': 'Delete',
'memDeleteConfirm': 'Delete this entry?',
'memFieldTitle': 'Content',
'memFieldDetail': 'Notes',
'memFieldDate': 'Date',
'memFieldTime': 'Time',
'memFieldAmount': 'Amount',
'memFieldOwner': 'Owner',
'memFieldRepeat': 'Repeat',
'memFieldRemind': 'Remind me',
'memRemindOff': 'No reminder',
'memRemindMinutes': '@n minutes before',
'memTitleRequired': 'Content cannot be empty',
'memAmountInvalid': 'Invalid amount',
'memSaveFailed': 'Save failed, please try again',
'memDeleteFailed': 'Delete failed, please try again',
'memCompleteFailed': 'Action failed, please try again',
'memOffline': 'Showing locally cached data',
'memFromMeeting': 'From meeting',
'memDatePending': 'Date to be set',
'memDueRaw': 'Due: @raw',
'memSummaryTodo': '@n to-do',
'memSummaryIdea': '@n idea',
'memSummaryAlarm': '@n alarm',
'memSummaryExpense': 'Spent @amount',
'memReminderTitle': 'Reminder',
'memNotifyDisabled': 'Notifications are off; reminders will only show while the app is open',
'memGoPermission': 'Settings',
'memReportWeekTitle': 'Last week',
'memReportMonthTitle': 'Last month',
'memReportConfirm': 'Got it',
'memReportPlay': 'Play',
'memReportStop': 'Stop',
'memDailyTitle': 'Today\'s plan',
'memDailyPlay': 'Play',
'memDailyEmpty': 'Nothing planned today',
'memDailyClose': 'Close',
'tabTodo': 'Amintiri',
// 通用
'continue': 'Continuă',
'confirm': 'Confirmă',

100
apps/client/lib/core/translations/language/ru_ru.dart

@ -1,5 +1,59 @@
// 俄罗斯-俄语
const Map<String, String> ruRU = {
'memDailyCount': 'You have @n item(s) today.',
'memCatAll': 'All',
'memCatTodo': 'To-do',
'memCatAlarm': 'Alarm',
'memCatIdea': 'Idea',
'memCatExpense': 'Expense',
'memCatOther': 'Other',
'memRepeatOnce': 'Once',
'memRepeatDaily': 'Daily',
'memRepeatWeekly': 'Weekly',
'memRepeatWeekdays': 'Weekdays',
'memRepeatWeekend': 'Weekends',
'memRepeatMonthly': 'Monthly',
'memEmptyTitle': 'Nothing recorded for this day',
'memEmptySubtitle': 'Tap + to add one, or ask EMAI to note it for you',
'memAddTitle': 'New',
'memEditTitle': 'Edit',
'memDelete': 'Delete',
'memDeleteConfirm': 'Delete this entry?',
'memFieldTitle': 'Content',
'memFieldDetail': 'Notes',
'memFieldDate': 'Date',
'memFieldTime': 'Time',
'memFieldAmount': 'Amount',
'memFieldOwner': 'Owner',
'memFieldRepeat': 'Repeat',
'memFieldRemind': 'Remind me',
'memRemindOff': 'No reminder',
'memRemindMinutes': '@n minutes before',
'memTitleRequired': 'Content cannot be empty',
'memAmountInvalid': 'Invalid amount',
'memSaveFailed': 'Save failed, please try again',
'memDeleteFailed': 'Delete failed, please try again',
'memCompleteFailed': 'Action failed, please try again',
'memOffline': 'Showing locally cached data',
'memFromMeeting': 'From meeting',
'memDatePending': 'Date to be set',
'memDueRaw': 'Due: @raw',
'memSummaryTodo': '@n to-do',
'memSummaryIdea': '@n idea',
'memSummaryAlarm': '@n alarm',
'memSummaryExpense': 'Spent @amount',
'memReminderTitle': 'Reminder',
'memNotifyDisabled': 'Notifications are off; reminders will only show while the app is open',
'memGoPermission': 'Settings',
'memReportWeekTitle': 'Last week',
'memReportMonthTitle': 'Last month',
'memReportConfirm': 'Got it',
'memReportPlay': 'Play',
'memReportStop': 'Stop',
'memDailyTitle': 'Today\'s plan',
'memDailyPlay': 'Play',
'memDailyEmpty': 'Nothing planned today',
'memDailyClose': 'Close',
// 通用
'continue': 'Продолжить',
'confirm': 'Подтвердить',
@ -1772,7 +1826,7 @@ const Map<String, String> ruRU = {
'binding': 'Привязка…',
'microphonePermission': 'Микрофон',
'tabDevice': 'Устройства',
'tabTodo': 'Remin',
'tabTodo': 'Воспоминания',
'tabPlaza': 'Плаза',
'sloganHome': 'Технологии для умной жизни',
'sloganAi': 'Безграничный интеллект',
@ -1924,50 +1978,6 @@ const Map<String, String> ruRU = {
'recordPause': 'Пауза',
'recordResume': 'Продолжить',
'recordDeleteConfirm': 'Удалить эту запись?',
'demoEvt1': 'Утренний английский',
'demoEvt2': 'Еженедельная встреча по проекту',
'demoEvt3': 'Напоминание пить воду',
'demoEvt4': 'Сдать еженедельный отчёт',
'demoEvt5': 'Тренировка: грудь и трицепс',
'demoEvt6': 'Чтение: Deep Work',
'demoEvt7': 'Повторный приём у стоматолога',
'demoEvt8': 'Завтрак',
'demoEvt9': 'Такси на работу',
'demoEvt10': 'Пополнить счёт телефона',
'demoEvt11': 'Билеты в кино',
'demoEvt12': 'Купить книгу',
'demoDesc1': '20 минут шэдоуинга, упор на произношение.',
'demoDesc2': 'Итоги недели и план на следующую.',
'demoDesc3': 'Каждые два часа.',
'demoDesc4': 'Отправлено на почту команды.',
'demoDesc5': '10 мин разминки, 40 мин рабочих подходов.',
'demoDesc6': 'Дочитать главу 3 и сделать заметки.',
'demoDesc7': 'Не забыть прошлое заключение.',
'demoDesc8': 'Соевое молоко и булочки.',
'demoDesc9': 'Такси в час пик, чтобы не ехать в переполненном метро.',
'demoDesc10': 'Ежемесячное автопополнение.',
'demoDesc11': 'Поздний сеанс с друзьями.',
'demoDesc12': 'Бумажное издание Deep Work.',
'demoLoc1': 'Кабинет',
'demoLoc2': 'Переговорная A',
'demoLoc4': 'Рабочее место',
'demoLoc5': 'Спортзал',
'demoLoc7': 'Стоматология',
'demoLoc8': 'Кафе на первом этаже',
'demoLoc9': 'Приложение такси',
'demoLoc10': 'Приложение оператора',
'demoLoc11': 'Кинотеатр',
'demoLoc12': 'Онлайн-книжный',
'demoTagStudy': 'Учёба',
'demoTagMeeting': 'Встреча',
'demoTagReminder': 'Напоминание',
'demoTagTask': 'Задача',
'demoTagSport': 'Спорт',
'demoTagBooking': 'Приём',
'demoTagFood': 'Еда',
'demoTagTransit': 'Транспорт',
'demoTagBills': 'Платежи',
'demoTagFun': 'Досуг',
'scopeDay': 'День',
'scopeWeek': 'Неделя',
'scopeMonth': 'Месяц',

55
apps/client/lib/core/translations/language/sk_sk.dart

@ -1,5 +1,60 @@
// 斯洛伐克-斯洛伐克语
const Map<String, String> skSK = {
'memDailyCount': 'You have @n item(s) today.',
'memCatAll': 'All',
'memCatTodo': 'To-do',
'memCatAlarm': 'Alarm',
'memCatIdea': 'Idea',
'memCatExpense': 'Expense',
'memCatOther': 'Other',
'memRepeatOnce': 'Once',
'memRepeatDaily': 'Daily',
'memRepeatWeekly': 'Weekly',
'memRepeatWeekdays': 'Weekdays',
'memRepeatWeekend': 'Weekends',
'memRepeatMonthly': 'Monthly',
'memEmptyTitle': 'Nothing recorded for this day',
'memEmptySubtitle': 'Tap + to add one, or ask EMAI to note it for you',
'memAddTitle': 'New',
'memEditTitle': 'Edit',
'memDelete': 'Delete',
'memDeleteConfirm': 'Delete this entry?',
'memFieldTitle': 'Content',
'memFieldDetail': 'Notes',
'memFieldDate': 'Date',
'memFieldTime': 'Time',
'memFieldAmount': 'Amount',
'memFieldOwner': 'Owner',
'memFieldRepeat': 'Repeat',
'memFieldRemind': 'Remind me',
'memRemindOff': 'No reminder',
'memRemindMinutes': '@n minutes before',
'memTitleRequired': 'Content cannot be empty',
'memAmountInvalid': 'Invalid amount',
'memSaveFailed': 'Save failed, please try again',
'memDeleteFailed': 'Delete failed, please try again',
'memCompleteFailed': 'Action failed, please try again',
'memOffline': 'Showing locally cached data',
'memFromMeeting': 'From meeting',
'memDatePending': 'Date to be set',
'memDueRaw': 'Due: @raw',
'memSummaryTodo': '@n to-do',
'memSummaryIdea': '@n idea',
'memSummaryAlarm': '@n alarm',
'memSummaryExpense': 'Spent @amount',
'memReminderTitle': 'Reminder',
'memNotifyDisabled': 'Notifications are off; reminders will only show while the app is open',
'memGoPermission': 'Settings',
'memReportWeekTitle': 'Last week',
'memReportMonthTitle': 'Last month',
'memReportConfirm': 'Got it',
'memReportPlay': 'Play',
'memReportStop': 'Stop',
'memDailyTitle': 'Today\'s plan',
'memDailyPlay': 'Play',
'memDailyEmpty': 'Nothing planned today',
'memDailyClose': 'Close',
'tabTodo': 'Spomienky',
// 通用
'continue': 'Pokračovať',
'confirm': 'Potvrdiť',

55
apps/client/lib/core/translations/language/sl_si.dart

@ -1,5 +1,60 @@
// 斯洛文尼亚-斯洛文尼亚语
const Map<String, String> slSI = {
'memDailyCount': 'You have @n item(s) today.',
'memCatAll': 'All',
'memCatTodo': 'To-do',
'memCatAlarm': 'Alarm',
'memCatIdea': 'Idea',
'memCatExpense': 'Expense',
'memCatOther': 'Other',
'memRepeatOnce': 'Once',
'memRepeatDaily': 'Daily',
'memRepeatWeekly': 'Weekly',
'memRepeatWeekdays': 'Weekdays',
'memRepeatWeekend': 'Weekends',
'memRepeatMonthly': 'Monthly',
'memEmptyTitle': 'Nothing recorded for this day',
'memEmptySubtitle': 'Tap + to add one, or ask EMAI to note it for you',
'memAddTitle': 'New',
'memEditTitle': 'Edit',
'memDelete': 'Delete',
'memDeleteConfirm': 'Delete this entry?',
'memFieldTitle': 'Content',
'memFieldDetail': 'Notes',
'memFieldDate': 'Date',
'memFieldTime': 'Time',
'memFieldAmount': 'Amount',
'memFieldOwner': 'Owner',
'memFieldRepeat': 'Repeat',
'memFieldRemind': 'Remind me',
'memRemindOff': 'No reminder',
'memRemindMinutes': '@n minutes before',
'memTitleRequired': 'Content cannot be empty',
'memAmountInvalid': 'Invalid amount',
'memSaveFailed': 'Save failed, please try again',
'memDeleteFailed': 'Delete failed, please try again',
'memCompleteFailed': 'Action failed, please try again',
'memOffline': 'Showing locally cached data',
'memFromMeeting': 'From meeting',
'memDatePending': 'Date to be set',
'memDueRaw': 'Due: @raw',
'memSummaryTodo': '@n to-do',
'memSummaryIdea': '@n idea',
'memSummaryAlarm': '@n alarm',
'memSummaryExpense': 'Spent @amount',
'memReminderTitle': 'Reminder',
'memNotifyDisabled': 'Notifications are off; reminders will only show while the app is open',
'memGoPermission': 'Settings',
'memReportWeekTitle': 'Last week',
'memReportMonthTitle': 'Last month',
'memReportConfirm': 'Got it',
'memReportPlay': 'Play',
'memReportStop': 'Stop',
'memDailyTitle': 'Today\'s plan',
'memDailyPlay': 'Play',
'memDailyEmpty': 'Nothing planned today',
'memDailyClose': 'Close',
'tabTodo': 'Spomini',
// 通用
'continue': 'Nadaljuj',
'confirm': 'Potrdi',

55
apps/client/lib/core/translations/language/sr_rs.dart

@ -1,5 +1,60 @@
// 塞尔维亚-塞尔维亚语
const Map<String, String> srRS = {
'memDailyCount': 'You have @n item(s) today.',
'memCatAll': 'All',
'memCatTodo': 'To-do',
'memCatAlarm': 'Alarm',
'memCatIdea': 'Idea',
'memCatExpense': 'Expense',
'memCatOther': 'Other',
'memRepeatOnce': 'Once',
'memRepeatDaily': 'Daily',
'memRepeatWeekly': 'Weekly',
'memRepeatWeekdays': 'Weekdays',
'memRepeatWeekend': 'Weekends',
'memRepeatMonthly': 'Monthly',
'memEmptyTitle': 'Nothing recorded for this day',
'memEmptySubtitle': 'Tap + to add one, or ask EMAI to note it for you',
'memAddTitle': 'New',
'memEditTitle': 'Edit',
'memDelete': 'Delete',
'memDeleteConfirm': 'Delete this entry?',
'memFieldTitle': 'Content',
'memFieldDetail': 'Notes',
'memFieldDate': 'Date',
'memFieldTime': 'Time',
'memFieldAmount': 'Amount',
'memFieldOwner': 'Owner',
'memFieldRepeat': 'Repeat',
'memFieldRemind': 'Remind me',
'memRemindOff': 'No reminder',
'memRemindMinutes': '@n minutes before',
'memTitleRequired': 'Content cannot be empty',
'memAmountInvalid': 'Invalid amount',
'memSaveFailed': 'Save failed, please try again',
'memDeleteFailed': 'Delete failed, please try again',
'memCompleteFailed': 'Action failed, please try again',
'memOffline': 'Showing locally cached data',
'memFromMeeting': 'From meeting',
'memDatePending': 'Date to be set',
'memDueRaw': 'Due: @raw',
'memSummaryTodo': '@n to-do',
'memSummaryIdea': '@n idea',
'memSummaryAlarm': '@n alarm',
'memSummaryExpense': 'Spent @amount',
'memReminderTitle': 'Reminder',
'memNotifyDisabled': 'Notifications are off; reminders will only show while the app is open',
'memGoPermission': 'Settings',
'memReportWeekTitle': 'Last week',
'memReportMonthTitle': 'Last month',
'memReportConfirm': 'Got it',
'memReportPlay': 'Play',
'memReportStop': 'Stop',
'memDailyTitle': 'Today\'s plan',
'memDailyPlay': 'Play',
'memDailyEmpty': 'Nothing planned today',
'memDailyClose': 'Close',
'tabTodo': 'Сећања',
// 通用
'continue': 'Nastavi',
'confirm': 'Potvrdi',

55
apps/client/lib/core/translations/language/sv_se.dart

@ -1,5 +1,60 @@
//瑞典-瑞典语
const Map<String, String> svSE = {
'memDailyCount': 'You have @n item(s) today.',
'memCatAll': 'All',
'memCatTodo': 'To-do',
'memCatAlarm': 'Alarm',
'memCatIdea': 'Idea',
'memCatExpense': 'Expense',
'memCatOther': 'Other',
'memRepeatOnce': 'Once',
'memRepeatDaily': 'Daily',
'memRepeatWeekly': 'Weekly',
'memRepeatWeekdays': 'Weekdays',
'memRepeatWeekend': 'Weekends',
'memRepeatMonthly': 'Monthly',
'memEmptyTitle': 'Nothing recorded for this day',
'memEmptySubtitle': 'Tap + to add one, or ask EMAI to note it for you',
'memAddTitle': 'New',
'memEditTitle': 'Edit',
'memDelete': 'Delete',
'memDeleteConfirm': 'Delete this entry?',
'memFieldTitle': 'Content',
'memFieldDetail': 'Notes',
'memFieldDate': 'Date',
'memFieldTime': 'Time',
'memFieldAmount': 'Amount',
'memFieldOwner': 'Owner',
'memFieldRepeat': 'Repeat',
'memFieldRemind': 'Remind me',
'memRemindOff': 'No reminder',
'memRemindMinutes': '@n minutes before',
'memTitleRequired': 'Content cannot be empty',
'memAmountInvalid': 'Invalid amount',
'memSaveFailed': 'Save failed, please try again',
'memDeleteFailed': 'Delete failed, please try again',
'memCompleteFailed': 'Action failed, please try again',
'memOffline': 'Showing locally cached data',
'memFromMeeting': 'From meeting',
'memDatePending': 'Date to be set',
'memDueRaw': 'Due: @raw',
'memSummaryTodo': '@n to-do',
'memSummaryIdea': '@n idea',
'memSummaryAlarm': '@n alarm',
'memSummaryExpense': 'Spent @amount',
'memReminderTitle': 'Reminder',
'memNotifyDisabled': 'Notifications are off; reminders will only show while the app is open',
'memGoPermission': 'Settings',
'memReportWeekTitle': 'Last week',
'memReportMonthTitle': 'Last month',
'memReportConfirm': 'Got it',
'memReportPlay': 'Play',
'memReportStop': 'Stop',
'memDailyTitle': 'Today\'s plan',
'memDailyPlay': 'Play',
'memDailyEmpty': 'Nothing planned today',
'memDailyClose': 'Close',
'tabTodo': 'Minnen',
// 通用
'continue': 'Fortsätt',
'confirm': 'Bekräfta',

55
apps/client/lib/core/translations/language/th_th.dart

@ -1,5 +1,60 @@
//泰国-泰语
const Map<String, String> thTH = {
'memDailyCount': 'You have @n item(s) today.',
'memCatAll': 'All',
'memCatTodo': 'To-do',
'memCatAlarm': 'Alarm',
'memCatIdea': 'Idea',
'memCatExpense': 'Expense',
'memCatOther': 'Other',
'memRepeatOnce': 'Once',
'memRepeatDaily': 'Daily',
'memRepeatWeekly': 'Weekly',
'memRepeatWeekdays': 'Weekdays',
'memRepeatWeekend': 'Weekends',
'memRepeatMonthly': 'Monthly',
'memEmptyTitle': 'Nothing recorded for this day',
'memEmptySubtitle': 'Tap + to add one, or ask EMAI to note it for you',
'memAddTitle': 'New',
'memEditTitle': 'Edit',
'memDelete': 'Delete',
'memDeleteConfirm': 'Delete this entry?',
'memFieldTitle': 'Content',
'memFieldDetail': 'Notes',
'memFieldDate': 'Date',
'memFieldTime': 'Time',
'memFieldAmount': 'Amount',
'memFieldOwner': 'Owner',
'memFieldRepeat': 'Repeat',
'memFieldRemind': 'Remind me',
'memRemindOff': 'No reminder',
'memRemindMinutes': '@n minutes before',
'memTitleRequired': 'Content cannot be empty',
'memAmountInvalid': 'Invalid amount',
'memSaveFailed': 'Save failed, please try again',
'memDeleteFailed': 'Delete failed, please try again',
'memCompleteFailed': 'Action failed, please try again',
'memOffline': 'Showing locally cached data',
'memFromMeeting': 'From meeting',
'memDatePending': 'Date to be set',
'memDueRaw': 'Due: @raw',
'memSummaryTodo': '@n to-do',
'memSummaryIdea': '@n idea',
'memSummaryAlarm': '@n alarm',
'memSummaryExpense': 'Spent @amount',
'memReminderTitle': 'Reminder',
'memNotifyDisabled': 'Notifications are off; reminders will only show while the app is open',
'memGoPermission': 'Settings',
'memReportWeekTitle': 'Last week',
'memReportMonthTitle': 'Last month',
'memReportConfirm': 'Got it',
'memReportPlay': 'Play',
'memReportStop': 'Stop',
'memDailyTitle': 'Today\'s plan',
'memDailyPlay': 'Play',
'memDailyEmpty': 'Nothing planned today',
'memDailyClose': 'Close',
'tabTodo': 'ความทรงจำ',
// 通用
'continue': 'ดำเนินการต่อ',
'confirm': 'ยืนยัน',

55
apps/client/lib/core/translations/language/tl_ph.dart

@ -1,5 +1,60 @@
//菲律宾-塔加路语
const Map<String, String> tlPH = {
'memDailyCount': 'You have @n item(s) today.',
'memCatAll': 'All',
'memCatTodo': 'To-do',
'memCatAlarm': 'Alarm',
'memCatIdea': 'Idea',
'memCatExpense': 'Expense',
'memCatOther': 'Other',
'memRepeatOnce': 'Once',
'memRepeatDaily': 'Daily',
'memRepeatWeekly': 'Weekly',
'memRepeatWeekdays': 'Weekdays',
'memRepeatWeekend': 'Weekends',
'memRepeatMonthly': 'Monthly',
'memEmptyTitle': 'Nothing recorded for this day',
'memEmptySubtitle': 'Tap + to add one, or ask EMAI to note it for you',
'memAddTitle': 'New',
'memEditTitle': 'Edit',
'memDelete': 'Delete',
'memDeleteConfirm': 'Delete this entry?',
'memFieldTitle': 'Content',
'memFieldDetail': 'Notes',
'memFieldDate': 'Date',
'memFieldTime': 'Time',
'memFieldAmount': 'Amount',
'memFieldOwner': 'Owner',
'memFieldRepeat': 'Repeat',
'memFieldRemind': 'Remind me',
'memRemindOff': 'No reminder',
'memRemindMinutes': '@n minutes before',
'memTitleRequired': 'Content cannot be empty',
'memAmountInvalid': 'Invalid amount',
'memSaveFailed': 'Save failed, please try again',
'memDeleteFailed': 'Delete failed, please try again',
'memCompleteFailed': 'Action failed, please try again',
'memOffline': 'Showing locally cached data',
'memFromMeeting': 'From meeting',
'memDatePending': 'Date to be set',
'memDueRaw': 'Due: @raw',
'memSummaryTodo': '@n to-do',
'memSummaryIdea': '@n idea',
'memSummaryAlarm': '@n alarm',
'memSummaryExpense': 'Spent @amount',
'memReminderTitle': 'Reminder',
'memNotifyDisabled': 'Notifications are off; reminders will only show while the app is open',
'memGoPermission': 'Settings',
'memReportWeekTitle': 'Last week',
'memReportMonthTitle': 'Last month',
'memReportConfirm': 'Got it',
'memReportPlay': 'Play',
'memReportStop': 'Stop',
'memDailyTitle': 'Today\'s plan',
'memDailyPlay': 'Play',
'memDailyEmpty': 'Nothing planned today',
'memDailyClose': 'Close',
'tabTodo': 'Alaala',
// 通用
'continue': 'Magpatuloy',
'confirm': 'Kumpirmahin',

55
apps/client/lib/core/translations/language/tr_tr.dart

@ -1,5 +1,60 @@
//土耳其-土耳其语
const Map<String, String> trTR = {
'memDailyCount': 'You have @n item(s) today.',
'memCatAll': 'All',
'memCatTodo': 'To-do',
'memCatAlarm': 'Alarm',
'memCatIdea': 'Idea',
'memCatExpense': 'Expense',
'memCatOther': 'Other',
'memRepeatOnce': 'Once',
'memRepeatDaily': 'Daily',
'memRepeatWeekly': 'Weekly',
'memRepeatWeekdays': 'Weekdays',
'memRepeatWeekend': 'Weekends',
'memRepeatMonthly': 'Monthly',
'memEmptyTitle': 'Nothing recorded for this day',
'memEmptySubtitle': 'Tap + to add one, or ask EMAI to note it for you',
'memAddTitle': 'New',
'memEditTitle': 'Edit',
'memDelete': 'Delete',
'memDeleteConfirm': 'Delete this entry?',
'memFieldTitle': 'Content',
'memFieldDetail': 'Notes',
'memFieldDate': 'Date',
'memFieldTime': 'Time',
'memFieldAmount': 'Amount',
'memFieldOwner': 'Owner',
'memFieldRepeat': 'Repeat',
'memFieldRemind': 'Remind me',
'memRemindOff': 'No reminder',
'memRemindMinutes': '@n minutes before',
'memTitleRequired': 'Content cannot be empty',
'memAmountInvalid': 'Invalid amount',
'memSaveFailed': 'Save failed, please try again',
'memDeleteFailed': 'Delete failed, please try again',
'memCompleteFailed': 'Action failed, please try again',
'memOffline': 'Showing locally cached data',
'memFromMeeting': 'From meeting',
'memDatePending': 'Date to be set',
'memDueRaw': 'Due: @raw',
'memSummaryTodo': '@n to-do',
'memSummaryIdea': '@n idea',
'memSummaryAlarm': '@n alarm',
'memSummaryExpense': 'Spent @amount',
'memReminderTitle': 'Reminder',
'memNotifyDisabled': 'Notifications are off; reminders will only show while the app is open',
'memGoPermission': 'Settings',
'memReportWeekTitle': 'Last week',
'memReportMonthTitle': 'Last month',
'memReportConfirm': 'Got it',
'memReportPlay': 'Play',
'memReportStop': 'Stop',
'memDailyTitle': 'Today\'s plan',
'memDailyPlay': 'Play',
'memDailyEmpty': 'Nothing planned today',
'memDailyClose': 'Close',
'tabTodo': 'Anılar',
// 通用
'continue': 'Devam Et',
'confirm': 'Onayla',

55
apps/client/lib/core/translations/language/uk_ua.dart

@ -1,5 +1,60 @@
// 乌克兰-乌克兰语
const Map<String, String> ukUA = {
'memDailyCount': 'You have @n item(s) today.',
'memCatAll': 'All',
'memCatTodo': 'To-do',
'memCatAlarm': 'Alarm',
'memCatIdea': 'Idea',
'memCatExpense': 'Expense',
'memCatOther': 'Other',
'memRepeatOnce': 'Once',
'memRepeatDaily': 'Daily',
'memRepeatWeekly': 'Weekly',
'memRepeatWeekdays': 'Weekdays',
'memRepeatWeekend': 'Weekends',
'memRepeatMonthly': 'Monthly',
'memEmptyTitle': 'Nothing recorded for this day',
'memEmptySubtitle': 'Tap + to add one, or ask EMAI to note it for you',
'memAddTitle': 'New',
'memEditTitle': 'Edit',
'memDelete': 'Delete',
'memDeleteConfirm': 'Delete this entry?',
'memFieldTitle': 'Content',
'memFieldDetail': 'Notes',
'memFieldDate': 'Date',
'memFieldTime': 'Time',
'memFieldAmount': 'Amount',
'memFieldOwner': 'Owner',
'memFieldRepeat': 'Repeat',
'memFieldRemind': 'Remind me',
'memRemindOff': 'No reminder',
'memRemindMinutes': '@n minutes before',
'memTitleRequired': 'Content cannot be empty',
'memAmountInvalid': 'Invalid amount',
'memSaveFailed': 'Save failed, please try again',
'memDeleteFailed': 'Delete failed, please try again',
'memCompleteFailed': 'Action failed, please try again',
'memOffline': 'Showing locally cached data',
'memFromMeeting': 'From meeting',
'memDatePending': 'Date to be set',
'memDueRaw': 'Due: @raw',
'memSummaryTodo': '@n to-do',
'memSummaryIdea': '@n idea',
'memSummaryAlarm': '@n alarm',
'memSummaryExpense': 'Spent @amount',
'memReminderTitle': 'Reminder',
'memNotifyDisabled': 'Notifications are off; reminders will only show while the app is open',
'memGoPermission': 'Settings',
'memReportWeekTitle': 'Last week',
'memReportMonthTitle': 'Last month',
'memReportConfirm': 'Got it',
'memReportPlay': 'Play',
'memReportStop': 'Stop',
'memDailyTitle': 'Today\'s plan',
'memDailyPlay': 'Play',
'memDailyEmpty': 'Nothing planned today',
'memDailyClose': 'Close',
'tabTodo': 'Спогади',
// 通用
'continue': 'Продовжити',
'confirm': 'Підтвердити',

55
apps/client/lib/core/translations/language/vi_vn.dart

@ -1,5 +1,60 @@
//越南-越南语
const Map<String, String> viVN = {
'memDailyCount': 'You have @n item(s) today.',
'memCatAll': 'All',
'memCatTodo': 'To-do',
'memCatAlarm': 'Alarm',
'memCatIdea': 'Idea',
'memCatExpense': 'Expense',
'memCatOther': 'Other',
'memRepeatOnce': 'Once',
'memRepeatDaily': 'Daily',
'memRepeatWeekly': 'Weekly',
'memRepeatWeekdays': 'Weekdays',
'memRepeatWeekend': 'Weekends',
'memRepeatMonthly': 'Monthly',
'memEmptyTitle': 'Nothing recorded for this day',
'memEmptySubtitle': 'Tap + to add one, or ask EMAI to note it for you',
'memAddTitle': 'New',
'memEditTitle': 'Edit',
'memDelete': 'Delete',
'memDeleteConfirm': 'Delete this entry?',
'memFieldTitle': 'Content',
'memFieldDetail': 'Notes',
'memFieldDate': 'Date',
'memFieldTime': 'Time',
'memFieldAmount': 'Amount',
'memFieldOwner': 'Owner',
'memFieldRepeat': 'Repeat',
'memFieldRemind': 'Remind me',
'memRemindOff': 'No reminder',
'memRemindMinutes': '@n minutes before',
'memTitleRequired': 'Content cannot be empty',
'memAmountInvalid': 'Invalid amount',
'memSaveFailed': 'Save failed, please try again',
'memDeleteFailed': 'Delete failed, please try again',
'memCompleteFailed': 'Action failed, please try again',
'memOffline': 'Showing locally cached data',
'memFromMeeting': 'From meeting',
'memDatePending': 'Date to be set',
'memDueRaw': 'Due: @raw',
'memSummaryTodo': '@n to-do',
'memSummaryIdea': '@n idea',
'memSummaryAlarm': '@n alarm',
'memSummaryExpense': 'Spent @amount',
'memReminderTitle': 'Reminder',
'memNotifyDisabled': 'Notifications are off; reminders will only show while the app is open',
'memGoPermission': 'Settings',
'memReportWeekTitle': 'Last week',
'memReportMonthTitle': 'Last month',
'memReportConfirm': 'Got it',
'memReportPlay': 'Play',
'memReportStop': 'Stop',
'memDailyTitle': 'Today\'s plan',
'memDailyPlay': 'Play',
'memDailyEmpty': 'Nothing planned today',
'memDailyClose': 'Close',
'tabTodo': 'Hồi ức',
// 通用
'continue': 'Tiếp tục',
'confirm': 'Xác nhận',

98
apps/client/lib/core/translations/language/zh_cn.dart

@ -1,5 +1,59 @@
//中国-简体中文
const Map<String, String> zhCN = {
'memDailyCount': '今天有 @n 件事。',
'memCatAll': '全部',
'memCatTodo': '待办',
'memCatAlarm': '闹钟',
'memCatIdea': '灵感',
'memCatExpense': '花销',
'memCatOther': '其他',
'memRepeatOnce': '不重复',
'memRepeatDaily': '每天',
'memRepeatWeekly': '每周',
'memRepeatWeekdays': '工作日',
'memRepeatWeekend': '周末',
'memRepeatMonthly': '每月',
'memEmptyTitle': '这一天还没有记录',
'memEmptySubtitle': '点右下角加一条,或让 EMAI 帮你记',
'memAddTitle': '新增',
'memEditTitle': '编辑',
'memDelete': '删除',
'memDeleteConfirm': '确定删除这一条?',
'memFieldTitle': '内容',
'memFieldDetail': '备注',
'memFieldDate': '日期',
'memFieldTime': '时间',
'memFieldAmount': '金额',
'memFieldOwner': '负责人',
'memFieldRepeat': '重复',
'memFieldRemind': '提前提醒',
'memRemindOff': '不提醒',
'memRemindMinutes': '提前 @n 分钟',
'memTitleRequired': '内容不能为空',
'memAmountInvalid': '金额格式不对',
'memSaveFailed': '保存失败,请重试',
'memDeleteFailed': '删除失败,请重试',
'memCompleteFailed': '操作失败,请重试',
'memOffline': '当前显示的是本地缓存',
'memFromMeeting': '来自会议',
'memDatePending': '待定日期',
'memDueRaw': '截止:@raw',
'memSummaryTodo': '@n 件待办',
'memSummaryIdea': '@n 条灵感',
'memSummaryAlarm': '@n 个闹钟',
'memSummaryExpense': '花销 @amount',
'memReminderTitle': '拾忆提醒',
'memNotifyDisabled': '通知权限未开启,提醒只能在打开 App 时显示',
'memGoPermission': '去设置',
'memReportWeekTitle': '上周回顾',
'memReportMonthTitle': '上月回顾',
'memReportConfirm': '知道了',
'memReportPlay': '播报',
'memReportStop': '停止',
'memDailyTitle': '今天的安排',
'memDailyPlay': '播报',
'memDailyEmpty': '今天没有安排',
'memDailyClose': '关闭',
// 通用
'continue': '继续',
'confirm': '确认',
@ -1735,50 +1789,6 @@ const Map<String, String> zhCN = {
'recordPause': '暂停',
'recordResume': '继续',
'recordDeleteConfirm': '确定删除这条录音吗?',
'demoEvt1': '晨间英语跟读',
'demoEvt2': '项目周会',
'demoEvt3': '喝水提醒',
'demoEvt4': '提交周报',
'demoEvt5': '健身:胸+三头',
'demoEvt6': '读书:深度工作',
'demoEvt7': '牙医复诊',
'demoEvt8': '早餐',
'demoEvt9': '打车上班',
'demoEvt10': '话费充值',
'demoEvt11': '电影票',
'demoEvt12': '买书',
'demoDesc1': '跟读 20 分钟,重点纠正音标。',
'demoDesc2': '同步本周进度与下周排期。',
'demoDesc3': '每两小时一次。',
'demoDesc4': '已发送至团队邮箱。',
'demoDesc5': '热身 10 分钟,正式组 40 分钟。',
'demoDesc6': '读完第 3 章并做笔记。',
'demoDesc7': '记得带上次的检查单。',
'demoDesc8': '豆浆 + 包子。',
'demoDesc9': '早高峰打车,避开地铁拥挤。',
'demoDesc10': '月度话费自动续费。',
'demoDesc11': '和朋友看晚场电影。',
'demoDesc12': '《深度工作》纸质版。',
'demoLoc1': '书房',
'demoLoc2': '会议室 A',
'demoLoc4': '工位',
'demoLoc5': '健身房',
'demoLoc7': '口腔医院',
'demoLoc8': '楼下早餐店',
'demoLoc9': '滴滴出行',
'demoLoc10': '运营商 App',
'demoLoc11': '万达影城',
'demoLoc12': '当当网',
'demoTagStudy': '学习',
'demoTagMeeting': '会议',
'demoTagReminder': '提醒',
'demoTagTask': '任务',
'demoTagSport': '运动',
'demoTagBooking': '预约',
'demoTagFood': '餐饮',
'demoTagTransit': '交通',
'demoTagBills': '生活缴费',
'demoTagFun': '娱乐',
'scopeDay': '日',
'scopeWeek': '周',
'scopeMonth': '月',

98
apps/client/lib/core/translations/language/zh_hk.dart

@ -1,5 +1,59 @@
//香港-繁体中文
const Map<String, String> zhHK = {
'memDailyCount': '今天有 @n 件事。',
'memCatAll': '全部',
'memCatTodo': '待辦',
'memCatAlarm': '鬧鐘',
'memCatIdea': '靈感',
'memCatExpense': '花銷',
'memCatOther': '其他',
'memRepeatOnce': '不重複',
'memRepeatDaily': '每天',
'memRepeatWeekly': '每週',
'memRepeatWeekdays': '工作日',
'memRepeatWeekend': '週末',
'memRepeatMonthly': '每月',
'memEmptyTitle': '這一天還沒有記錄',
'memEmptySubtitle': '點右下角加一條,或讓 EMAI 幫你記',
'memAddTitle': '新增',
'memEditTitle': '編輯',
'memDelete': '刪除',
'memDeleteConfirm': '確定刪除這一條?',
'memFieldTitle': '內容',
'memFieldDetail': '備註',
'memFieldDate': '日期',
'memFieldTime': '時間',
'memFieldAmount': '金額',
'memFieldOwner': '負責人',
'memFieldRepeat': '重複',
'memFieldRemind': '提前提醒',
'memRemindOff': '不提醒',
'memRemindMinutes': '提前 @n 分鐘',
'memTitleRequired': '內容不能為空',
'memAmountInvalid': '金額格式不對',
'memSaveFailed': '儲存失敗,請重試',
'memDeleteFailed': '刪除失敗,請重試',
'memCompleteFailed': '操作失敗,請重試',
'memOffline': '目前顯示的是本機快取',
'memFromMeeting': '來自會議',
'memDatePending': '待定日期',
'memDueRaw': '截止:@raw',
'memSummaryTodo': '@n 件待辦',
'memSummaryIdea': '@n 條靈感',
'memSummaryAlarm': '@n 個鬧鐘',
'memSummaryExpense': '花銷 @amount',
'memReminderTitle': '拾憶提醒',
'memNotifyDisabled': '通知權限未開啟,提醒只能在打開 App 時顯示',
'memGoPermission': '去設定',
'memReportWeekTitle': '上週回顧',
'memReportMonthTitle': '上月回顧',
'memReportConfirm': '知道了',
'memReportPlay': '播報',
'memReportStop': '停止',
'memDailyTitle': '今天的安排',
'memDailyPlay': '播報',
'memDailyEmpty': '今天沒有安排',
'memDailyClose': '關閉',
// 通用
'continue': '繼續',
'confirm': '確認',
@ -1710,50 +1764,6 @@ const Map<String, String> zhHK = {
'recordPause': '暫停',
'recordResume': '繼續',
'recordDeleteConfirm': '確定要刪除呢個錄音?',
'demoEvt1': '晨早英語跟讀',
'demoEvt2': '項目週會',
'demoEvt3': '飲水提醒',
'demoEvt4': '交週報',
'demoEvt5': '健身:胸+三頭',
'demoEvt6': '睇書:《深度工作》',
'demoEvt7': '牙醫覆診',
'demoEvt8': '早餐',
'demoEvt9': '搭車返工',
'demoEvt10': '電話費增值',
'demoEvt11': '戲飛',
'demoEvt12': '買書',
'demoDesc1': '跟讀 20 分鐘,重點改正音標。',
'demoDesc2': '同步今個星期進度同下星期安排。',
'demoDesc3': '每兩個鐘一次。',
'demoDesc4': '已寄咗去團隊郵箱。',
'demoDesc5': '熱身 10 分鐘,正式組 40 分鐘。',
'demoDesc6': '睇完第 3 章同埋做筆記。',
'demoDesc7': '記得帶上次張檢查單。',
'demoDesc8': '豆漿+包。',
'demoDesc9': '繁忙時間搭車,避開地鐵逼。',
'demoDesc10': '每月電話費自動續期。',
'demoDesc11': '同朋友睇夜場戲。',
'demoDesc12': '《深度工作》實體書。',
'demoLoc1': '書房',
'demoLoc2': '會議室 A',
'demoLoc4': '座位',
'demoLoc5': '健身室',
'demoLoc7': '牙科診所',
'demoLoc8': '樓下茶記',
'demoLoc9': '叫車 App',
'demoLoc10': '電訊商 App',
'demoLoc11': '戲院',
'demoLoc12': '網上書店',
'demoTagStudy': '學習',
'demoTagMeeting': '會議',
'demoTagReminder': '提醒',
'demoTagTask': '任務',
'demoTagSport': '運動',
'demoTagBooking': '預約',
'demoTagFood': '飲食',
'demoTagTransit': '交通',
'demoTagBills': '生活繳費',
'demoTagFun': '娛樂',
'scopeDay': '日',
'scopeWeek': '週',
'scopeMonth': '月',

98
apps/client/lib/core/translations/language/zh_tw.dart

@ -1,5 +1,59 @@
//台湾-繁体中文
const Map<String, String> zhTW = {
'memDailyCount': '今天有 @n 件事。',
'memCatAll': '全部',
'memCatTodo': '待辦',
'memCatAlarm': '鬧鐘',
'memCatIdea': '靈感',
'memCatExpense': '花銷',
'memCatOther': '其他',
'memRepeatOnce': '不重複',
'memRepeatDaily': '每天',
'memRepeatWeekly': '每週',
'memRepeatWeekdays': '工作日',
'memRepeatWeekend': '週末',
'memRepeatMonthly': '每月',
'memEmptyTitle': '這一天還沒有記錄',
'memEmptySubtitle': '點右下角加一條,或讓 EMAI 幫你記',
'memAddTitle': '新增',
'memEditTitle': '編輯',
'memDelete': '刪除',
'memDeleteConfirm': '確定刪除這一條?',
'memFieldTitle': '內容',
'memFieldDetail': '備註',
'memFieldDate': '日期',
'memFieldTime': '時間',
'memFieldAmount': '金額',
'memFieldOwner': '負責人',
'memFieldRepeat': '重複',
'memFieldRemind': '提前提醒',
'memRemindOff': '不提醒',
'memRemindMinutes': '提前 @n 分鐘',
'memTitleRequired': '內容不能為空',
'memAmountInvalid': '金額格式不對',
'memSaveFailed': '儲存失敗,請重試',
'memDeleteFailed': '刪除失敗,請重試',
'memCompleteFailed': '操作失敗,請重試',
'memOffline': '目前顯示的是本機快取',
'memFromMeeting': '來自會議',
'memDatePending': '待定日期',
'memDueRaw': '截止:@raw',
'memSummaryTodo': '@n 件待辦',
'memSummaryIdea': '@n 條靈感',
'memSummaryAlarm': '@n 個鬧鐘',
'memSummaryExpense': '花銷 @amount',
'memReminderTitle': '拾憶提醒',
'memNotifyDisabled': '通知權限未開啟,提醒只能在打開 App 時顯示',
'memGoPermission': '去設定',
'memReportWeekTitle': '上週回顧',
'memReportMonthTitle': '上月回顧',
'memReportConfirm': '知道了',
'memReportPlay': '播報',
'memReportStop': '停止',
'memDailyTitle': '今天的安排',
'memDailyPlay': '播報',
'memDailyEmpty': '今天沒有安排',
'memDailyClose': '關閉',
// 通用
'continue': '繼續',
'confirm': '確認',
@ -1710,50 +1764,6 @@ const Map<String, String> zhTW = {
'recordPause': '暫停',
'recordResume': '繼續',
'recordDeleteConfirm': '確定刪除這則錄音嗎?',
'demoEvt1': '晨間英語跟讀',
'demoEvt2': '專案週會',
'demoEvt3': '喝水提醒',
'demoEvt4': '提交週報',
'demoEvt5': '健身:胸+三頭',
'demoEvt6': '閱讀:《深度工作》',
'demoEvt7': '牙醫複診',
'demoEvt8': '早餐',
'demoEvt9': '搭車上班',
'demoEvt10': '電信費儲值',
'demoEvt11': '電影票',
'demoEvt12': '買書',
'demoDesc1': '跟讀 20 分鐘,重點糾正音標。',
'demoDesc2': '同步本週進度與下週排程。',
'demoDesc3': '每兩小時一次。',
'demoDesc4': '已寄送至團隊信箱。',
'demoDesc5': '熱身 10 分鐘,正式組 40 分鐘。',
'demoDesc6': '讀完第 3 章並做筆記。',
'demoDesc7': '記得帶上次的檢查單。',
'demoDesc8': '豆漿+包子。',
'demoDesc9': '尖峰時段搭車,避開捷運人潮。',
'demoDesc10': '每月電信費自動續約。',
'demoDesc11': '和朋友看晚場電影。',
'demoDesc12': '《深度工作》紙本版。',
'demoLoc1': '書房',
'demoLoc2': '會議室 A',
'demoLoc4': '座位',
'demoLoc5': '健身房',
'demoLoc7': '牙醫診所',
'demoLoc8': '樓下早餐店',
'demoLoc9': '叫車 App',
'demoLoc10': '電信業者 App',
'demoLoc11': '影城',
'demoLoc12': '網路書店',
'demoTagStudy': '學習',
'demoTagMeeting': '會議',
'demoTagReminder': '提醒',
'demoTagTask': '任務',
'demoTagSport': '運動',
'demoTagBooking': '預約',
'demoTagFood': '餐飲',
'demoTagTransit': '交通',
'demoTagBills': '生活繳費',
'demoTagFun': '娛樂',
'scopeDay': '日',
'scopeWeek': '週',
'scopeMonth': '月',

35
apps/client/lib/core/utils/app_lifecycle_manager.dart

@ -7,6 +7,10 @@ import 'package:get_storage/get_storage.dart';
import '../../modules/meeting/controllers/meeting_record_controller.dart';
import '../../routes/app_routes.dart';
import 'logger.dart';
import '../../data/models/user_Info.dart';
import '../../data/services/memory_reminder_scheduler.dart';
import '../../data/services/memory_service.dart';
import '../../modules/main_tab/views/widgets/memory_daily_sheet.dart';
import '../services/native_localization_sync.dart';
import '../translations/app_translations.dart';
//import '../../modules/settings/controllers/settings_controller.dart';
@ -110,6 +114,15 @@ class AppLifecycleManager extends GetxService with WidgetsBindingObserver {
/// 处理应用进入前台
void _handleAppResumed() {
// 拾忆:切回前台时重排提醒 + 视情况弹每日安排。
//
// 挂在这里而不是各页面自己 addObserver:仓库里已经有
// emai / translation / permissions 三个各自实现的 WidgetsBindingObserver,
// 再加一个没有意义,本类就是收敛点。
//
// ⚠️ 一律 unawaited。这条回调在每次切前台都跑,不能被网络请求拖住。
unawaited(_refreshMemoryOnResume());
// Logger.info('应用进入前台');
// 这里不需要主动恢复服务,因为我们希望用户手动控制录音开始
// 如果正在录音,停止录音
@ -122,6 +135,28 @@ class AppLifecycleManager extends GetxService with WidgetsBindingObserver {
// }
}
/// 切前台时的拾忆刷新:重排本地提醒,并在每天首次时弹「今天的安排」。
///
/// 失败一律吞掉——这是锦上添花的能力,不能因为它把切前台这条路径搞出异常。
Future<void> _refreshMemoryOnResume() async {
if (!Get.isRegistered<MemoryService>()) return;
// 未登录时拿不到数据,直接跳过(登录后由登录流程自己触发一次)
if (!User.isLoggedIn()) return;
try {
// 先拉当天数据顺便更新缓存,再决定弹不弹
await MemoryDailySheet.maybeShow();
} catch (e) {
Logger.warning('拾忆每日弹窗失败(已忽略): $e');
}
try {
if (Get.isRegistered<MemoryReminderScheduler>()) {
await MemoryReminderScheduler.to.reschedule();
}
} catch (e) {
Logger.warning('拾忆提醒重排失败(已忽略): $e');
}
}
/// 处理应用进入后台
void _handleAppPaused(AppLifecycleState state) {
// Logger.info('应用进入后台: $state');

396
apps/client/lib/data/models/memory_item.dart

@ -0,0 +1,396 @@
import 'package:flutter/material.dart';
/// 记忆项分类。
///
/// ⚠️ **不要用 enum 做解析口径**:服务端的分类是字符串白名单,加新分类只改服务端
/// 常量、不发新包。客户端一旦 switch 不到就抛,服务端加一个分类就会把所有存量
/// App 的拾忆页整页打崩。所以这里保留原始字符串,展示层一律经 [MemoryCategory.of]
/// 兜底成 todo。
class MemoryCategory {
static const String alarm = 'alarm';
static const String todo = 'todo';
static const String idea = 'idea';
static const String expense = 'expense';
/// 本版认识的分类,按拾忆页筛选条的展示顺序
static const List<String> known = [todo, alarm, idea, expense];
/// 认不出来的分类一律当 todo 渲染。这是「服务端可以随时加分类」的前提。
static String of(String raw) => known.contains(raw) ? raw : todo;
static bool isKnown(String raw) => known.contains(raw);
/// 分类配色。未知分类给中性灰,不要复用某个已有分类的颜色——
/// 那会让新分类伪装成旧分类。
static Color colorOf(String raw) {
switch (raw) {
case todo:
return const Color(0xFF5B8DEF);
case alarm:
return const Color(0xFF22C1A6);
case idea:
return const Color(0xFFF59E0B);
case expense:
return const Color(0xFFEC4899);
default:
return const Color(0xFF94A3B8);
}
}
/// i18n key
static String labelKey(String raw) {
switch (raw) {
case todo:
return 'memCatTodo';
case alarm:
return 'memCatAlarm';
case idea:
return 'memCatIdea';
case expense:
return 'memCatExpense';
default:
return 'memCatOther';
}
}
}
/// 记忆项来源
class MemorySource {
static const String meeting = 'meeting';
static const String assistant = 'assistant';
static const String manual = 'manual';
static const String device = 'device';
}
/// 重复规则
class MemoryRepeat {
static const String once = 'once';
static const String daily = 'daily';
static const String weekly = 'weekly';
static const String weekdays = 'weekdays';
static const String weekend = 'weekend';
static const String monthly = 'monthly';
static const List<String> all = [once, daily, weekly, weekdays, weekend, monthly];
static String labelKey(String raw) {
switch (raw) {
case daily:
return 'memRepeatDaily';
case weekly:
return 'memRepeatWeekly';
case weekdays:
return 'memRepeatWeekdays';
case weekend:
return 'memRepeatWeekend';
case monthly:
return 'memRepeatMonthly';
default:
return 'memRepeatOnce';
}
}
}
/// 状态:0 待办 / 1 完成 / 2 废弃
class MemoryState {
static const int pending = 0;
static const int done = 1;
static const int dropped = 2;
}
/// 一条记忆项。手写镜像服务端 `pb.DBMemoryItem`。
///
/// ⚠️ **每个字段都必须能吃下缺失值**。服务端删一个字段、或某条老记录没有某列时,
/// non-nullable 字段会让整个 fromJson 抛 TypeError,表现是拾忆页整体空白而不是
/// 少一个字段——`appconfig_model.dart` 已经为此在服务端留了兼容层,这里从一开始
/// 就用带默认值的解析,不重蹈覆辙。
class MemoryItem {
final int id;
final String category;
final String source;
final String sourceId;
final String clientKey;
String title;
String detail;
/// 归属日期 `yyyy-MM-dd`,服务端保证永不为空
String happenDate;
/// `HH:mm`,无具体时间则为空串
String happenTime;
/// false = 日期是推断的(比如会议待办没写截止时间,落在会议当天)。
/// 这类项要单独归到「待定日期」分组,不能和当天确定事项混排——
/// 一次会抽出 8 条全堆在同一天,那一格就没法看了。
bool dateCertain;
/// 截止时间原文(「下周三前」),服务端推不出具体日期时靠它表达
String dueRaw;
/// 录入时的时区(IANA 名)
String tz;
String repeatRule;
String repeatRaw;
int weekday;
/// 花销金额,**单位是分**。与服务端 pay/结算模块同口径。
/// 展示时除以 100,不要在别处再引入一套「元」的表示法。
int amountCents;
String currency;
String owner;
int state;
int remindAhead;
int nextRemindAt;
bool userEdited;
final int genRound;
final String extra;
final int createTime;
final int updateTime;
final int finishTime;
MemoryItem({
this.id = 0,
this.category = MemoryCategory.todo,
this.source = MemorySource.manual,
this.sourceId = '',
this.clientKey = '',
this.title = '',
this.detail = '',
this.happenDate = '',
this.happenTime = '',
this.dateCertain = true,
this.dueRaw = '',
this.tz = '',
this.repeatRule = MemoryRepeat.once,
this.repeatRaw = '',
this.weekday = 0,
this.amountCents = 0,
this.currency = 'CNY',
this.owner = '',
this.state = MemoryState.pending,
this.remindAhead = 5,
this.nextRemindAt = 0,
this.userEdited = false,
this.genRound = 0,
this.extra = '',
this.createTime = 0,
this.updateTime = 0,
this.finishTime = 0,
});
bool get isDone => state == MemoryState.done;
bool get isExpense => category == MemoryCategory.expense;
bool get isRepeating => repeatRule.isNotEmpty && repeatRule != MemoryRepeat.once;
/// 金额的显示值(元)。只在展示层用,存储与传输一律用分。
double get amountYuan => amountCents / 100.0;
/// 解析出的日期。服务端保证 happenDate 非空且合法,但真拿到脏数据时
/// 退回今天而不是抛——一条坏记录不该让整页渲染不出来。
DateTime get happenDateTime =>
DateTime.tryParse(happenDate) ?? DateTime.now();
TimeOfDay? get startTime {
if (happenTime.isEmpty) return null;
final parts = happenTime.split(':');
if (parts.length != 2) return null;
final h = int.tryParse(parts[0]);
final m = int.tryParse(parts[1]);
if (h == null || m == null) return null;
if (h < 0 || h > 23 || m < 0 || m > 59) return null;
return TimeOfDay(hour: h, minute: m);
}
static int _int(dynamic v, [int fallback = 0]) => memInt(v, fallback);
static bool _bool(dynamic v, [bool fallback = false]) => memBool(v, fallback);
static String _str(dynamic v, [String fallback = '']) => memStr(v, fallback);
factory MemoryItem.fromJson(Map<String, dynamic> json) => MemoryItem(
id: _int(json['id']),
category: _str(json['category'], MemoryCategory.todo),
source: _str(json['source'], MemorySource.manual),
sourceId: _str(json['source_id']),
clientKey: _str(json['client_key']),
title: _str(json['title']),
detail: _str(json['detail']),
happenDate: _str(json['happen_date']),
happenTime: _str(json['happen_time']),
dateCertain: _bool(json['date_certain'], true),
dueRaw: _str(json['due_raw']),
tz: _str(json['tz']),
repeatRule: _str(json['repeat_rule'], MemoryRepeat.once),
repeatRaw: _str(json['repeat_raw']),
weekday: _int(json['weekday']),
amountCents: _int(json['amount_cents']),
currency: _str(json['currency'], 'CNY'),
owner: _str(json['owner']),
state: _int(json['state']),
remindAhead: _int(json['remind_ahead'], 5),
nextRemindAt: _int(json['next_remind_at']),
userEdited: _bool(json['user_edited']),
genRound: _int(json['gen_round']),
extra: _str(json['extra']),
createTime: _int(json['create_time']),
updateTime: _int(json['update_time']),
finishTime: _int(json['finish_time']),
);
Map<String, dynamic> toJson() => {
'id': id,
'category': category,
'source': source,
'source_id': sourceId,
'client_key': clientKey,
'title': title,
'detail': detail,
'happen_date': happenDate,
'happen_time': happenTime,
'date_certain': dateCertain,
'due_raw': dueRaw,
'tz': tz,
'repeat_rule': repeatRule,
'repeat_raw': repeatRaw,
'weekday': weekday,
'amount_cents': amountCents,
'currency': currency,
'owner': owner,
'state': state,
'remind_ahead': remindAhead,
'next_remind_at': nextRemindAt,
'user_edited': userEdited,
'gen_round': genRound,
'extra': extra,
'create_time': createTime,
'update_time': updateTime,
'finish_time': finishTime,
};
MemoryItem copy() => MemoryItem.fromJson(toJson());
}
/// 一个展开后的提醒时刻(服务端已按项自己的时区算好,客户端直接排通知)
class MemoryRemindSlot {
final int itemId;
final int remindAt;
final int happenAt;
final String title;
final String detail;
final String category;
MemoryRemindSlot({
required this.itemId,
required this.remindAt,
required this.happenAt,
required this.title,
required this.detail,
required this.category,
});
factory MemoryRemindSlot.fromJson(Map<String, dynamic> json) =>
MemoryRemindSlot(
itemId: MemoryItem._int(json['item_id']),
remindAt: MemoryItem._int(json['remind_at']),
happenAt: MemoryItem._int(json['happen_at']),
title: MemoryItem._str(json['title']),
detail: MemoryItem._str(json['detail']),
category: MemoryItem._str(json['category'], MemoryCategory.todo),
);
DateTime get remindDateTime =>
DateTime.fromMillisecondsSinceEpoch(remindAt * 1000);
}
/// 单币种花销小计。服务端按币种分行给出,**不做汇率换算**——
/// 把 CNY 和 JPY 加成一个数是错的。
class MemoryExpenseSum {
final String currency;
final int totalCents;
final int count;
MemoryExpenseSum({
required this.currency,
required this.totalCents,
required this.count,
});
double get totalYuan => totalCents / 100.0;
factory MemoryExpenseSum.fromJson(Map<String, dynamic> json) =>
MemoryExpenseSum(
currency: MemoryItem._str(json['currency'], 'CNY'),
totalCents: MemoryItem._int(json['total_cents']),
count: MemoryItem._int(json['count']),
);
}
/// 周期报告
class MemoryReport {
final int id;
final String periodType; // week / month
final String periodKey; // 2026-W35 / 2026-08
final String periodStart;
final String periodEnd;
final String statJson;
final String summary;
final int state;
final String errorMsg;
bool confirmed;
MemoryReport({
this.id = 0,
this.periodType = 'week',
this.periodKey = '',
this.periodStart = '',
this.periodEnd = '',
this.statJson = '',
this.summary = '',
this.state = 0,
this.errorMsg = '',
this.confirmed = false,
});
bool get isWeek => periodType == 'week';
factory MemoryReport.fromJson(Map<String, dynamic> json) => MemoryReport(
id: MemoryItem._int(json['id']),
periodType: MemoryItem._str(json['period_type'], 'week'),
periodKey: MemoryItem._str(json['period_key']),
periodStart: MemoryItem._str(json['period_start']),
periodEnd: MemoryItem._str(json['period_end']),
statJson: MemoryItem._str(json['stat_json']),
summary: MemoryItem._str(json['summary']),
state: MemoryItem._int(json['state']),
errorMsg: MemoryItem._str(json['error_msg']),
confirmed: MemoryItem._bool(json['confirmed']),
);
}
// ===== 宽松解析器 =====
//
// 全部字段都经这三个函数解析,不直接 `json['x'] as int`。
// 服务端删字段、老记录缺列、数字被序列化成字符串——任何一种都不该让整个
// fromJson 抛 TypeError。appconfig_model 那边就是因为 non-nullable 字段
// 让整份配置解析失败,表现成设备列表整体空白而不是少一个字段。
int memInt(dynamic v, [int fallback = 0]) {
if (v is int) return v;
if (v is num) return v.toInt();
if (v is String) return int.tryParse(v) ?? fallback;
return fallback;
}
bool memBool(dynamic v, [bool fallback = false]) {
if (v is bool) return v;
if (v is num) return v != 0;
if (v is String) return v == 'true' || v == '1';
return fallback;
}
String memStr(dynamic v, [String fallback = '']) =>
v == null ? fallback : v.toString();

18
apps/client/lib/data/services/account_service.dart

@ -1,3 +1,5 @@
import 'dart:async';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import 'package:logger/logger.dart';
@ -8,6 +10,8 @@ import 'db/database_helper.dart';
import '../../data/models/user_Info.dart';
import 'meeting/meeting_upload_service.dart';
import 'bes_device_auth.dart';
import 'memory_reminder_scheduler.dart';
import 'memory_service.dart';
/// 账号管理服务
///
@ -38,6 +42,14 @@ class AccountService extends GetxService {
// 换账号后同一台耳机要重新登记到新账号名下,
// 不清这个 set 的话「本进程已登记过」会把新账号的绑定吞掉。
BesDeviceAuth.reset();
// 拾忆是账号数据:不清的话换账号后会看到上一个人的日程、花销和灵感,
// 排定的提醒也会继续按上一个账号的安排响。
if (Get.isRegistered<MemoryService>()) {
MemoryService.to.clear();
}
if (Get.isRegistered<MemoryReminderScheduler>()) {
unawaited(MemoryReminderScheduler.to.clear());
}
logger.d('用户数据清理成功');
return true;
} catch (e) {
@ -52,6 +64,12 @@ class AccountService extends GetxService {
await _storage.erase();
User.clearUserSession();
BesDeviceAuth.reset();
if (Get.isRegistered<MemoryService>()) {
MemoryService.to.clear();
}
if (Get.isRegistered<MemoryReminderScheduler>()) {
unawaited(MemoryReminderScheduler.to.clear());
}
logger.d('所有用户数据清理成功');
return true;
} catch (e) {

73
apps/client/lib/data/services/assistant_directive_service.dart

@ -1,8 +1,13 @@
import 'dart:async';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import '../../core/utils/logger.dart';
import '../models/assistant_directive.dart';
import '../models/memory_item.dart';
import '../models/user_Info.dart';
import 'memory_service.dart';
/// 助手指令的落地层:**解析 → 分发 → 落本地记录**。
///
@ -10,7 +15,9 @@ import '../models/assistant_directive.dart';
/// 解出来,App 内对话页(EmaiController)和耳机唤醒(BesAiSessionService)两条
/// 链路都会往这里送,这里是唯一的汇合点。
///
/// 目前只有 `SET_clock` 真正落地(落成提醒,展示在「代办」页)。其余指令一律
/// 目前只有 `SET_clock` 真正落地:**双写**——本地 reminders(迁移期保留一个版本)
/// 加服务端 memory_item(category=alarm),于是它会出现在拾忆页并跨设备可见。
/// 其余指令一律
/// 记一条 `unsupported` 流水——**不是丢掉**:将来要按这份流水看用户到底在用
/// 哪些能力、以及百炼后台配了但客户端没实现的有哪些。
///
@ -115,6 +122,70 @@ class AssistantDirectiveService extends GetxService {
d.note = reminder.repeat == RepeatRule.unknown && reminder.repeatRaw.isNotEmpty
? '重复规则「${reminder.repeatRaw}」未识别,按单次处理'
: null;
// 同时写服务端,让它出现在拾忆页并跨设备可见。
//
// 迁移期**双写**:本地 reminders 保留一个版本,等确认服务端链路稳了再摘。
// 这样即使上传失败,用户至少还能在本机看到自己刚定的闹钟。
unawaited(_uploadReminder(reminder));
}
/// 把一条助手落下来的闹钟写进记忆中心。
///
/// ⚠️ **幂等键用指令 id**:端侧指令会重试,而本类的按 id 去重只在**本进程内存**里
/// ([_seen]),换手机、重装、进程重启后全部失效。服务端靠 (uid, client_key)
/// 唯一索引兜住,重复提交返回既有记录而不是插第二条。
Future<void> _uploadReminder(AssistantReminder r) async {
if (!Get.isRegistered<MemoryService>()) return;
if (!User.isLoggedIn()) return;
try {
final item = MemoryItem(
category: MemoryCategory.alarm,
source: MemorySource.assistant,
sourceId: r.directiveId,
clientKey: r.directiveId.isNotEmpty ? r.directiveId : r.id,
title: r.content.isNotEmpty ? r.content : 'memCatAlarm'.tr,
happenDate: r.date ?? _todayString(),
happenTime: r.time,
repeatRule: _repeatToRaw(r.repeat),
repeatRaw: r.repeatRaw,
weekday: r.weekday ?? 0,
remindAhead: 5,
);
final saved = await MemoryService.to.add(item);
if (saved != null) {
r.synced = true;
_persist();
}
} catch (e) {
// 上传失败不影响本地闹钟——迁移期本地那份仍然是可用的
Logger.w(_tag, '闹钟上传服务端失败(本地仍可用): $e');
}
}
static String _todayString() {
final d = DateTime.now();
return '${d.year}-${d.month.toString().padLeft(2, '0')}-'
'${d.day.toString().padLeft(2, '0')}';
}
/// 本地 RepeatRule 枚举 → 服务端的字符串口径
static String _repeatToRaw(RepeatRule r) {
switch (r) {
case RepeatRule.daily:
return MemoryRepeat.daily;
case RepeatRule.weekly:
return MemoryRepeat.weekly;
case RepeatRule.weekdays:
return MemoryRepeat.weekdays;
case RepeatRule.weekend:
return MemoryRepeat.weekend;
// unknown 按单次处理——与上面 d.note 的口径一致,
// 认不出的重复规则宁可只响一次,也不要按猜的规则天天响。
case RepeatRule.once:
case RepeatRule.unknown:
return MemoryRepeat.once;
}
}
/// 用户在代办页勾选完成

254
apps/client/lib/data/services/memory_reminder_scheduler.dart

@ -0,0 +1,254 @@
import 'dart:convert';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:flutter_timezone/flutter_timezone.dart';
import 'package:get/get.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:timezone/data/latest_all.dart' as tzdata;
import 'package:timezone/timezone.dart' as tz;
import '../../core/utils/logger.dart';
import '../models/memory_item.dart';
import 'memory_service.dart';
/// 拾忆的提前提醒:把服务端展开好的提醒时刻排成系统本地通知。
///
/// ## 为什么在客户端做
/// App 未打开、甚至无网时也要响。服务端推送做不到——没有 APNs/FCM 通道,
/// 国内厂商推送还要逐家接。
///
/// ## ⚠️ 送达率做不到 100%,这是已知的
/// 三条叠加,都不是代码能绕过去的:
/// 1. 通知权限有 48 小时合规冷却(见 PermissionUtil):用户在别处拒过一次,
/// 两天内提醒完全静默,而且 App **不被允许**弹窗解释。
/// 2. Android 14 起 SCHEDULE_EXACT_ALARM 对普通应用默认不授予。真正该用的是
/// USE_EXACT_ALARM,但 Google Play 要求「应用核心功能就是闹钟/日历」并单独
/// 提交说明——那是产品决策。拿不到精确权限时这里退化成 inexact 排期,
/// 「提前 5 分钟」会变成「提前 5 到 20 分钟之间」。
/// 3. 国内 ROM 的省电策略会清掉未加白名单应用的 AlarmManager 排期。
///
/// ## ⚠️ 通知 id 必须分命名空间
/// 仓库里已经有两个各自 new 出来的 FlutterLocalNotificationsPlugin 实例
/// (background_session_notifier / meeting_record_controller),各管各的 id。
/// 提醒占 [_idBase] 起的一段固定区间,取消时**逐条 cancel**,
/// **绝不调 cancelAll()** —— 那会顺手干掉正在录音的前台通知,
/// 表现是录音悄悄失去前台身份、被系统回收。
class MemoryReminderScheduler extends GetxService {
static MemoryReminderScheduler get to => Get.find<MemoryReminderScheduler>();
static const String _tag = 'MemoryReminder';
/// 提醒通知的 id 区间:[_idBase, _idBase + _maxScheduled)
static const int _idBase = 920000;
/// 排期上限。iOS 最多 64 条待处理通知,留一些余量给别的通知用。
static const int _maxScheduled = 48;
/// 只排未来这么多天
static const int _days = 7;
final FlutterLocalNotificationsPlugin _plugin =
FlutterLocalNotificationsPlugin();
bool _pluginReady = false;
bool _tzReady = false;
/// 上一次真正排出去的 id,取消时按它逐条撤
final List<int> _scheduledIds = [];
/// 权限被拒时置 true,页面据此显示一条静态提示(不是弹窗)指向权限管理页
final RxBool permissionDenied = false.obs;
Future<void> _ensureTz() async {
if (_tzReady) return;
tzdata.initializeTimeZones();
try {
final name = await FlutterTimezone.getLocalTimezone();
tz.setLocalLocation(tz.getLocation(name.toString()));
} catch (e) {
// 拿不到系统时区就用 tz 的默认 UTC 之外的兜底:按本机偏移找一个等价区。
// 不做这一步会让所有提醒按 UTC 排,国内用户全部偏 8 小时。
Logger.w(_tag, '读系统时区失败,按本机偏移兜底: $e');
try {
final offset = DateTime.now().timeZoneOffset;
final match = tz.timeZoneDatabase.locations.values.firstWhere(
(l) => l.currentTimeZone.offset == offset.inMilliseconds,
orElse: () => tz.getLocation('Asia/Shanghai'),
);
tz.setLocalLocation(match);
} catch (_) {
tz.setLocalLocation(tz.getLocation('Asia/Shanghai'));
}
}
_tzReady = true;
}
Future<void> _ensurePlugin() async {
if (_pluginReady) return;
const androidSettings =
AndroidInitializationSettings('@mipmap/ic_launcher');
const iosSettings = DarwinInitializationSettings(
// 不在这里请求权限:请求要走 PermissionUtil 那套 48 小时冷却,
// 绕过它等于绕过合规规则。
requestAlertPermission: false,
requestBadgePermission: false,
requestSoundPermission: false,
);
await _plugin.initialize(
const InitializationSettings(android: androidSettings, iOS: iosSettings),
onDidReceiveNotificationResponse: _onTap,
);
_pluginReady = true;
}
void _onTap(NotificationResponse response) {
final payload = response.payload;
if (payload == null || payload.isEmpty) return;
try {
final data = jsonDecode(payload) as Map<String, dynamic>;
final itemId = memInt(data['item_id']);
Logger.d(_tag, '用户点开提醒 item:$itemId');
// 播报交给拾忆页:通知里不直接出声,锁屏状态下强行播报是骚扰。
Get.toNamed('/main', arguments: {'tab': 'memory', 'focusItem': itemId});
} catch (e) {
Logger.w(_tag, '解析提醒 payload 失败: $e');
}
}
/// 通知权限是否已授予。
///
/// ⚠️ **只查不申请**。申请必须走 PermissionUtil 的 requestPermission(它收敛了
/// 48 小时冷却),而且只允许在「设置 → 权限管理」里由用户主动发起。
/// 这里主动申请会直接违反那条合规规则。
Future<bool> _hasPermission() async {
try {
final status = await Permission.notification.status;
final ok = status.isGranted;
permissionDenied.value = !ok;
return ok;
} catch (e) {
Logger.w(_tag, '查询通知权限失败: $e');
return false;
}
}
/// 重新排期:先撤旧的,再按服务端给的时刻排新的。
///
/// 每次都全撤全排,不做增量 diff —— 改期后旧通知还在响比多排一次贵得多。
Future<void> reschedule() async {
if (!await _hasPermission()) {
Logger.i(_tag, '无通知权限,降级为「打开 App 才提醒」');
await _cancelAllOurs();
return;
}
await _ensureTz();
await _ensurePlugin();
final slots = await MemoryService.to.loadUpcoming(days: _days);
await _cancelAllOurs();
if (slots.isEmpty) {
Logger.d(_tag, '没有待排的提醒');
return;
}
// 服务端已按 remind_at 升序返回。按顺序取前 N 条 —— 顺序错了会把明天的
// 排掉、留下一周后的,所以这里不再自己排序,直接信任服务端口径。
final now = DateTime.now();
var idx = 0;
for (final slot in slots) {
if (idx >= _maxScheduled) break;
final when = slot.remindDateTime;
if (!when.isAfter(now)) continue;
final id = _idBase + idx;
try {
await _plugin.zonedSchedule(
id,
slot.title.isEmpty ? 'memReminderTitle'.tr : slot.title,
_bodyOf(slot),
tz.TZDateTime.from(when, tz.local),
_details(),
androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle,
// 按「墙上时钟时间」解释:用户设的是 8:00,跨时区飞行后也该是当地 8:00 语义
// 由服务端按项自己的 tz 算好绝对时刻,这里只需保证不被二次偏移。
uiLocalNotificationDateInterpretation:
UILocalNotificationDateInterpretation.absoluteTime,
payload: jsonEncode({'item_id': slot.itemId}),
);
_scheduledIds.add(id);
idx++;
} catch (e) {
// 拿不到精确闹钟权限(Android 14 常见)会在这里抛。退化成 inexact
// 再试一次:不精确的提醒也比没有强。
try {
await _plugin.zonedSchedule(
id,
slot.title.isEmpty ? 'memReminderTitle'.tr : slot.title,
_bodyOf(slot),
tz.TZDateTime.from(when, tz.local),
_details(),
androidScheduleMode: AndroidScheduleMode.inexactAllowWhileIdle,
uiLocalNotificationDateInterpretation:
UILocalNotificationDateInterpretation.absoluteTime,
payload: jsonEncode({'item_id': slot.itemId}),
);
_scheduledIds.add(id);
idx++;
Logger.w(_tag, '精确排期失败已退化为 inexact(提醒时间可能延后): $e');
} catch (e2) {
Logger.e(_tag, '排期失败 item:${slot.itemId}: $e2');
}
}
}
Logger.i(_tag, '已排定 $idx 条提醒(共 ${slots.length} 个待选时刻)');
}
String _bodyOf(MemoryRemindSlot slot) {
if (slot.detail.isNotEmpty) return slot.detail;
final t = DateTime.fromMillisecondsSinceEpoch(slot.happenAt * 1000);
return '${t.hour.toString().padLeft(2, '0')}:'
'${t.minute.toString().padLeft(2, '0')}';
}
NotificationDetails _details() {
// 单独的通道:录音提示是常驻低优先级通道,共用会让提醒被降级成静默。
const android = AndroidNotificationDetails(
'memory_reminder',
'拾忆提醒',
channelDescription: '待办与闹钟的提前提醒',
importance: Importance.high,
priority: Priority.high,
);
const ios = DarwinNotificationDetails(
presentAlert: true,
presentBadge: true,
presentSound: true,
);
return const NotificationDetails(android: android, iOS: ios);
}
/// 只撤我们自己排的那些 id。
///
/// ⚠️ **绝不能用 cancelAll()**:它会把录音的前台通知一起干掉。
Future<void> _cancelAllOurs() async {
if (!_pluginReady) return;
for (final id in _scheduledIds) {
try {
await _plugin.cancel(id);
} catch (_) {}
}
// 进程重启后 _scheduledIds 是空的,但系统里可能还留着上次的排期,
// 所以把整个区间都撤一遍(cancel 不存在的 id 是安全的空操作)。
for (var i = 0; i < _maxScheduled; i++) {
try {
await _plugin.cancel(_idBase + i);
} catch (_) {}
}
_scheduledIds.clear();
}
/// 登出时清掉所有提醒——换账号不该继续响上一个账号的日程
Future<void> clear() async {
await _ensurePlugin();
await _cancelAllOurs();
}
}

428
apps/client/lib/data/services/memory_service.dart

@ -0,0 +1,428 @@
import 'dart:convert';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import '../../core/utils/logger.dart';
import '../models/memory_item.dart';
import 'network/api.dart';
/// 记忆中心(拾忆)的数据层。
///
/// 服务端是唯一真相源,本地 GetStorage 只做缓存与离线兜底。
///
/// ⚠️ **只缓存最近 [_cacheDays] 天**,不做全量镜像。GetStorage 是把整个 box 读进
/// 内存的 JSON 文件,记忆项一年三千多条,全量缓存会让每次启动越来越慢。
class MemoryService extends GetxService {
static MemoryService get to => Get.find<MemoryService>();
static const String _tag = 'MemoryService';
static const String _kItems = 'memory_items_cache';
static const String _kCacheDate = 'memory_items_cache_date';
/// 本地缓存保留的天数(以今天为中心,前后各这么多天)
static const int _cacheDays = 90;
final _storage = GetStorage();
/// 当前已加载的记忆项。页面 Obx 直接读它。
final RxList<MemoryItem> items = <MemoryItem>[].obs;
/// 是否正在拉取(用于列表下拉刷新的转圈,不用于全屏遮罩)
final RxBool loading = false.obs;
/// 最近一次拉取是否失败。失败时页面显示「显示的是本地缓存」而不是空白。
final RxBool staleFromCache = false.obs;
/// 待确认的周期报告 id,0 = 没有
final RxInt pendingReportId = 0.obs;
final RxString pendingReportType = ''.obs;
@override
void onInit() {
super.onInit();
_restore();
}
// ===================== 本地缓存 =====================
void _restore() {
try {
final raw = _storage.read<List>(_kItems);
if (raw == null) return;
final list = raw
.map((e) => MemoryItem.fromJson(Map<String, dynamic>.from(e as Map)))
.toList();
items.assignAll(list);
Logger.d(_tag, '恢复本地缓存 ${list.length} 条');
} catch (e) {
// 缓存结构变了(比如加了字段)就整份丢掉重拉,别让一份坏缓存把页面卡死
Logger.w(_tag, '本地缓存解析失败,已清空: $e');
items.clear();
_storage.remove(_kItems);
}
}
void _persist() {
try {
final today = DateTime.now();
final from = today.subtract(const Duration(days: _cacheDays));
final to = today.add(const Duration(days: _cacheDays));
final kept = items.where((e) {
final d = e.happenDateTime;
return d.isAfter(from) && d.isBefore(to);
}).toList();
_storage.write(_kItems, kept.map((e) => e.toJson()).toList());
_storage.write(_kCacheDate, _fmtDate(today));
} catch (e) {
Logger.w(_tag, '写本地缓存失败(不影响本次使用): $e');
}
}
/// 登出时清空。记忆项是账号数据,换账号必须清干净。
void clear() {
items.clear();
pendingReportId.value = 0;
pendingReportType.value = '';
staleFromCache.value = false;
_storage.remove(_kItems);
_storage.remove(_kCacheDate);
}
// ===================== 工具 =====================
static String _fmtDate(DateTime d) =>
'${d.year.toString().padLeft(4, '0')}-'
'${d.month.toString().padLeft(2, '0')}-'
'${d.day.toString().padLeft(2, '0')}';
/// 本机时区名。拿不到就传空串——服务端会退到它自己的本地时区,
/// **不会**退成 UTC(那会让所有时刻静默偏移 8 小时)。
String _tz() {
try {
return DateTime.now().timeZoneName;
} catch (_) {
return '';
}
}
/// 把服务端返回的 data 取成 Map。
///
/// ⚠️ AuthInterceptor 在 code==0 且 data 为空时把 data 换成了 `{}` 而不是 null,
/// 在业务错误时才把 data 置成 null 并 resolve(**不抛**)。所以:
/// resp == null → 服务端明确返回了业务错误
/// resp == {} → 成功但没数据
/// 两者含义相反,绝不能合并判断。
Map<String, dynamic>? _data(dynamic resp) {
if (resp == null) return null;
if (resp.data == null) return null;
if (resp.data is Map) return Map<String, dynamic>.from(resp.data as Map);
return null;
}
List<MemoryItem> _parseItems(dynamic raw) {
if (raw is! List) return [];
final out = <MemoryItem>[];
for (final e in raw) {
try {
out.add(MemoryItem.fromJson(Map<String, dynamic>.from(e as Map)));
} catch (err) {
// 单条解析失败只跳过这一条。一条脏数据不该让整页空白——
// appconfig_model 那边就是因为 non-nullable 字段让整个 fromJson 挂掉,
// 表现成设备列表整体不显示。
Logger.w(_tag, '跳过一条无法解析的记忆项: $err');
}
}
return out;
}
// ===================== 读 =====================
/// 拉取日期范围内的记忆项,结果**合并**进 [items](不是整体替换)。
///
/// 合并而不是替换:日历会按需拉不同月份,整体替换会让上个月的数据在切回去时消失。
Future<bool> load({
DateTime? start,
DateTime? end,
List<String>? categories,
bool silent = false,
}) async {
final s = start ?? DateTime.now().subtract(const Duration(days: 30));
final e = end ?? DateTime.now().add(const Duration(days: 60));
if (!silent) loading.value = true;
try {
final resp = await Api.memoryList({
'start_date': _fmtDate(s),
'end_date': _fmtDate(e),
if (categories != null && categories.isNotEmpty) 'categories': categories,
'tz': _tz(),
'size': 1000,
});
final data = _data(resp);
if (data == null) {
staleFromCache.value = true;
Logger.w(_tag, '拉取记忆项失败(业务错误),沿用本地缓存');
return false;
}
final fetched = _parseItems(data['items']);
_mergeRange(fetched, _fmtDate(s), _fmtDate(e));
staleFromCache.value = false;
_persist();
Logger.d(_tag, '拉取 ${fetched.length} 条 (${_fmtDate(s)} ~ ${_fmtDate(e)})');
return true;
} catch (err) {
// 网络层异常:保留本地缓存,页面标成「离线」,绝不清空
staleFromCache.value = true;
Logger.w(_tag, '拉取记忆项异常,沿用本地缓存: $err');
return false;
} finally {
if (!silent) loading.value = false;
}
}
/// 用服务端返回的这一段替换掉本地同一段,段外的保持不动
void _mergeRange(List<MemoryItem> fetched, String start, String end) {
final kept = items.where((e) {
final d = e.happenDate;
return d.compareTo(start) < 0 || d.compareTo(end) > 0;
}).toList();
kept.addAll(fetched);
kept.sort(_compare);
items.assignAll(kept);
}
static int _compare(MemoryItem a, MemoryItem b) {
final c = a.happenDate.compareTo(b.happenDate);
if (c != 0) return c;
// 无时刻的排在当天最后(与服务端 listItems 的排序口径一致)
if (a.happenTime.isEmpty != b.happenTime.isEmpty) {
return a.happenTime.isEmpty ? 1 : -1;
}
final t = a.happenTime.compareTo(b.happenTime);
if (t != 0) return t;
return a.id.compareTo(b.id);
}
/// 当天事项(每日弹窗用)。顺带带回待确认报告标记。
///
/// ⚠️ **调用方不要 await 在启动路径上**。冷启动已经串了四次网络往返
/// (网络检查 → 版本检查 → getLoginToken → getAppConfig),再挂一次会把
/// 「页面立刻可见」这条刚做过的优化又推回去。进主页后异步拉、拿到再弹。
Future<List<MemoryItem>> loadToday() async {
try {
final resp = await Api.memoryToday({
'date': _fmtDate(DateTime.now()),
'tz': _tz(),
});
final data = _data(resp);
if (data == null) return _localToday();
final list = _parseItems(data['items']);
pendingReportId.value = memInt(data['pending_report_id']);
pendingReportType.value = (data['pending_report_type'] ?? '').toString();
_mergeRange(list, _fmtDate(DateTime.now()), _fmtDate(DateTime.now()));
_persist();
return list;
} catch (e) {
Logger.w(_tag, 'loadToday 异常,退回本地缓存: $e');
return _localToday();
}
}
List<MemoryItem> _localToday() {
final today = _fmtDate(DateTime.now());
return items.where((e) => e.happenDate == today).toList();
}
/// 未来 N 天的提醒时刻(重复项已由服务端展开成绝对时刻)
Future<List<MemoryRemindSlot>> loadUpcoming({int days = 7}) async {
try {
final resp = await Api.memoryUpcoming({'days': days, 'tz': _tz()});
final data = _data(resp);
if (data == null) return [];
final raw = data['slots'];
if (raw is! List) return [];
return raw
.map((e) => MemoryRemindSlot.fromJson(Map<String, dynamic>.from(e as Map)))
.toList();
} catch (e) {
Logger.w(_tag, 'loadUpcoming 异常: $e');
return [];
}
}
// ===================== 写 =====================
/// 新增。成功返回落库后的记录(含服务端分配的 id),失败返回 null。
Future<MemoryItem?> add(MemoryItem item) async {
try {
item.tz = item.tz.isEmpty ? _tz() : item.tz;
final resp = await Api.memoryAdd({'item': item.toJson()});
final data = _data(resp);
if (data == null) return null;
final saved = MemoryItem.fromJson(
Map<String, dynamic>.from(data['item'] as Map));
// 幂等命中时服务端返回既有记录,这里按 id 去重后再插,避免列表里出现两条
items.removeWhere((e) => e.id == saved.id);
items.add(saved);
items.sort(_compare);
items.refresh();
_persist();
return saved;
} catch (e) {
Logger.e(_tag, '新增失败: $e');
return null;
}
}
/// 修改。fields 点名要改的字段(json 名),空 = 按服务端白名单全量覆盖。
Future<MemoryItem?> update(MemoryItem item, {List<String>? fields}) async {
try {
final resp = await Api.memoryUpdate({
'id': item.id,
'item': item.toJson(),
if (fields != null && fields.isNotEmpty) 'fields': fields,
});
final data = _data(resp);
if (data == null) return null;
final saved = MemoryItem.fromJson(
Map<String, dynamic>.from(data['item'] as Map));
final idx = items.indexWhere((e) => e.id == saved.id);
if (idx >= 0) {
items[idx] = saved;
} else {
items.add(saved);
}
items.sort(_compare);
items.refresh();
_persist();
return saved;
} catch (e) {
Logger.e(_tag, '修改失败: $e');
return null;
}
}
/// 删除
Future<bool> remove(List<int> ids) async {
if (ids.isEmpty) return true;
try {
final resp = await Api.memoryDel({'ids': ids});
if (_data(resp) == null) return false;
items.removeWhere((e) => ids.contains(e.id));
items.refresh();
_persist();
return true;
} catch (e) {
Logger.e(_tag, '删除失败: $e');
return false;
}
}
/// 标记完成 / 退回未完成。
///
/// 先改本地再发请求(乐观更新):勾一下要等一次往返才变灰太迟钝。
/// 失败时**回滚**——不回滚会让用户以为已经勾上了,下次进页面又变回来。
Future<bool> complete(List<int> ids, bool done) async {
if (ids.isEmpty) return true;
final before = <int, int>{};
for (final e in items) {
if (ids.contains(e.id)) {
before[e.id] = e.state;
e.state = done ? MemoryState.done : MemoryState.pending;
}
}
items.refresh();
try {
final resp = await Api.memoryComplete({'ids': ids, 'done': done});
if (_data(resp) == null) throw Exception('业务错误');
_persist();
return true;
} catch (e) {
for (final it in items) {
if (before.containsKey(it.id)) it.state = before[it.id]!;
}
items.refresh();
Logger.e(_tag, '标记完成失败,已回滚: $e');
return false;
}
}
// ===================== 周期报告 =====================
/// 取最近一份未确认的报告。
///
/// 服务端在「生成中」时返回 MemoryReportNotReady(5205),AuthInterceptor 会把
/// data 置 null,这里拿到 null 就当「暂时没有」——**不要弹错**,报告还在生成
/// 不是故障。
Future<MemoryReport?> loadPendingReport() async {
try {
final resp = await Api.memoryGetReport({});
final data = _data(resp);
if (data == null) return null;
final r = data['report'];
if (r == null) return null;
return MemoryReport.fromJson(Map<String, dynamic>.from(r as Map));
} catch (e) {
Logger.w(_tag, 'loadPendingReport 异常: $e');
return null;
}
}
Future<bool> confirmReport(int reportId) async {
try {
final resp = await Api.memoryConfirmReport({'report_id': reportId});
if (_data(resp) == null) return false;
if (pendingReportId.value == reportId) {
pendingReportId.value = 0;
pendingReportType.value = '';
}
return true;
} catch (e) {
Logger.e(_tag, '确认报告失败: $e');
return false;
}
}
// ===================== 查询辅助(页面用)=====================
List<MemoryItem> forDate(DateTime day, {String? category}) {
final d = _fmtDate(day);
return items
.where((e) =>
e.happenDate == d &&
(category == null || category.isEmpty || e.category == category))
.toList();
}
List<MemoryItem> forRange(DateTime from, DateTime to, {String? category}) {
final f = _fmtDate(from);
final t = _fmtDate(to);
return items
.where((e) =>
e.happenDate.compareTo(f) >= 0 &&
e.happenDate.compareTo(t) <= 0 &&
(category == null || category.isEmpty || e.category == category))
.toList();
}
/// 范围内的花销小计,**按币种分组**。不做汇率换算。
Map<String, int> expenseCentsByCurrency(DateTime from, DateTime to) {
final out = <String, int>{};
for (final e in forRange(from, to)) {
if (!e.isExpense) continue;
out[e.currency] = (out[e.currency] ?? 0) + e.amountCents;
}
return out;
}
/// 解析报告的 stat_json。坏数据返回空 Map,不抛。
static Map<String, dynamic> parseStat(String statJson) {
if (statJson.isEmpty) return {};
try {
final v = jsonDecode(statJson);
return v is Map ? Map<String, dynamic>.from(v) : {};
} catch (_) {
return {};
}
}
}

103
apps/client/lib/data/services/network/api.dart

@ -61,6 +61,109 @@ class Api {
}
}
// ===== 记忆中心(拾忆)=====
// 路径规律:/api/home/<模块>_<方法小写>,由 gateway 的 /api/:param1/:param2 接住
// (param1 就是服务名 home),服务端 memory 模块靠反射注册,两边都不用改路由表。
/// 拉取记忆项:start_date / end_date / categories / states / tz / page / size
static memoryList([params]) {
return DioManager().request(
NWMethod.post,
'/api/home/memory_list',
params: params,
);
}
/// 当天事项 + 待确认报告标记:date / tz
static memoryToday([params]) {
return DioManager().request(
NWMethod.post,
'/api/home/memory_today',
params: params,
);
}
/// 新增:{item: {...}},item.client_key 非空时服务端做幂等
static memoryAdd([params]) {
return DioManager().request(
NWMethod.post,
'/api/home/memory_add',
params: params,
);
}
/// 修改:{id, item, fields},服务端自动置 user_edited
static memoryUpdate([params]) {
return DioManager().request(
NWMethod.post,
'/api/home/memory_update',
params: params,
);
}
/// 删除:{ids: [...]}
static memoryDel([params]) {
return DioManager().request(
NWMethod.post,
'/api/home/memory_del',
params: params,
);
}
/// 标记完成/退回:{ids: [...], done: bool}
static memoryComplete([params]) {
return DioManager().request(
NWMethod.post,
'/api/home/memory_complete',
params: params,
);
}
/// 未来 N 天待提醒项(重复项已由服务端展开成绝对时刻):days / tz
static memoryUpcoming([params]) {
return DioManager().request(
NWMethod.post,
'/api/home/memory_upcoming',
params: params,
);
}
/// 统计:start_date / end_date / categories
static memoryStats([params]) {
return DioManager().request(
NWMethod.post,
'/api/home/memory_stats',
params: params,
);
}
/// 取周报/月报;period_type 与 period_key 都不传 = 最近一份未确认的
static memoryGetReport([params]) {
return DioManager().request(
NWMethod.post,
'/api/home/memory_getreport',
params: params,
);
}
/// 确认报告:{report_id}
static memoryConfirmReport([params]) {
return DioManager().request(
NWMethod.post,
'/api/home/memory_confirmreport',
params: params,
);
}
/// 历史报告列表:period_type / page / size
static memoryListReports([params]) {
return DioManager().request(
NWMethod.post,
'/api/home/memory_listreports',
params: params,
);
}
// 登录
static login([params]) {
return DioManager().request(

7
apps/client/lib/data/services/network/auth_interceptor.dart

@ -23,8 +23,13 @@ class AuthInterceptor extends Interceptor {
// 恒玄链路(BesDeviceAuth)要弹的是「设备连接受限」,杰理的手动绑定按钮
// (DevicesController.bindAndConnect)弹的是 deviceUnauthorized。
// 不挡住的话用户会先看到一条后端的英文内部状态词(AuthorizeNoCanUse)。
// memory_today / memory_upcoming 是**启动与前台恢复时自动调**的:用户什么都没点
// 就先吃一条红条(消息还是后端的英文内部状态词)体验极差。拾忆页改用行内空态/
// 错误态表达,出错就静默降级成本地缓存。
return path == '/api/home/user_getinfo' ||
path == '/api/home/user_binddevice';
path == '/api/home/user_binddevice' ||
path == '/api/home/memory_today' ||
path == '/api/home/memory_upcoming';
}
/// 后端在未登录时会给一些接口返回业务错误码,msg 字段就是英文内部状态词

145
apps/client/lib/modules/home/controllers/calendar_controller.dart

@ -3,8 +3,9 @@ import 'package:intl/intl.dart';
import 'package:flutter/material.dart';
import 'package:lunar/lunar.dart';
import '../../../data/models/assistant_directive.dart';
import '../../../data/services/assistant_directive_service.dart';
import '../../../core/utils/logger.dart';
import '../../../data/models/memory_item.dart';
import '../../../data/services/memory_service.dart';
import '../views/widgets/calendar/calendar_event.dart';
/// 日历视图格式:月视图 / 周视图
@ -22,9 +23,12 @@ enum CalendarScope {
final String label;
}
/// 首页日历面板控制器(移植自 AI-study-machine 的 CalendarController)。
/// 拾忆页日历面板控制器(移植自 AI-study-machine 的 CalendarController)。
///
/// 几何/手势/折叠/翻页逻辑原样保留;数据层换成内存 mock(不接后端/不落库)。
/// 几何/手势/折叠/翻页逻辑原样保留;**数据层已换成服务端**——
/// 唯一来源是 [MemoryService.items],本地 GetStorage 只做缓存与离线兜底。
/// 演示数据 `mockCalendarEvents()` 已删除:真实数据上线后混着 mock
/// 会让人以为系统出错。
class CalendarController extends GetxController
with GetSingleTickerProviderStateMixin {
// ==================== Rx 状态 ====================
@ -46,13 +50,21 @@ class CalendarController extends GetxController
final RxList<CalendarEvent> events = <CalendarEvent>[].obs;
/// 监听助手提醒变化的 worker,onClose 要停掉
Worker? _reminderWorker;
Worker? _memoryWorker;
// 折叠态横向条的代办/花销筛选:null=全部,非空=只看该类别。
// 点击"代办"/"花销"标签设置;点击日期(onDaySelected)重置为全部。
final Rx<CalendarEventCategory?> categoryFilter =
Rx<CalendarEventCategory?>(null);
/// 设置分类筛选,null = 全部。
///
/// 与 [setCategoryFilter] 分开:那个是「点某一类」的语义(同类再点一次会取消),
/// 这个是筛选条直接指定,包含「全部」这一档。
void setCategoryFilterOrAll(CalendarEventCategory? category) {
categoryFilter.value = category;
}
void setCategoryFilter(CalendarEventCategory category) {
categoryFilter.value = category;
}
@ -99,68 +111,86 @@ class CalendarController extends GetxController
vsync: this,
duration: const Duration(milliseconds: _snapDurationMaxMs),
)..addListener(_onSnapAnimationTick);
// 加载内存 mock 数据(不接后端/不落库)
_rebuildEvents();
// EMAI 助手定的闹钟落地后要立刻出现在这里,不用退出重进
if (Get.isRegistered<AssistantDirectiveService>()) {
_reminderWorker = ever<List<AssistantReminder>>(
AssistantDirectiveService.to.reminders,
// 服务端数据变了(拉取完成、用户增删改、EMAI 落了新闹钟)就立刻重投影,
// 不用退出重进页面。
if (Get.isRegistered<MemoryService>()) {
_memoryWorker = ever<List<MemoryItem>>(
MemoryService.to.items,
(_) => _rebuildEvents(),
);
}
}
/// mock 演示数据 + EMAI 助手落下来的提醒
/// 从 [MemoryService] 单一来源投影成日历事件。
///
/// 重复项在服务端是**一行记录**(repeat_rule=daily/weekly…),日历要按天展示,
/// 所以在可见窗口内逐日摊开——这段展开逻辑是从原来的助手提醒那套留用的,
/// 只是输入从 AssistantReminder 换成了 MemoryItem。
void _rebuildEvents() {
final list = <CalendarEvent>[...mockCalendarEvents()];
if (Get.isRegistered<AssistantDirectiveService>()) {
for (final r in AssistantDirectiveService.to.reminders) {
list.addAll(_expandReminder(r));
}
if (!Get.isRegistered<MemoryService>()) {
events.clear();
return;
}
final list = <CalendarEvent>[];
for (final item in MemoryService.to.items) {
list.addAll(_expandItem(item));
}
events.assignAll(list);
}
/// 把一条提醒摊成日历事件。
/// 把一条记忆项摊成日历事件。
///
/// 重复提醒在协议里只有一条记录(`repeat=每天` / `周一`),日历要按天展示,
/// 所以在可见窗口内逐日摊开。窗口取「今天前 7 天 ~ 后 60 天」:往前留几天是
/// 为了让用户还能看到刚过去的提醒,往后 60 天覆盖月视图翻两页。
List<CalendarEvent> _expandReminder(AssistantReminder r) {
final title = r.content.isNotEmpty ? r.content : 'alarm'.tr;
final start = TimeOfDay(hour: r.hour, minute: r.minute);
/// 窗口取「今天前 7 天 ~ 后 60 天」:往前留几天是为了让用户还能看到刚过去的
/// 事项,往后 60 天覆盖月视图翻两页。
List<CalendarEvent> _expandItem(MemoryItem item) {
final anchor = item.happenDateTime;
final cat = CalendarEventCategoryX.fromRaw(item.category);
final color = MemoryCategory.colorOf(item.category);
CalendarEvent make(DateTime day, String suffix) => CalendarEvent(
id: '${r.id}$suffix',
title: title,
// 重复规则原文放描述里,用户能看懂"每周一"比看懂 weekly 容易
description: r.repeatRaw,
id: '${item.id}$suffix',
memoryId: item.id,
title: item.title,
// 负责人与截止原文合成一行副标题;两者都空时留空而不是塞占位符
description: [item.owner, item.dueRaw]
.where((e) => e.isNotEmpty)
.join(' · '),
date: DateTime(day.year, day.month, day.day),
startTime: start,
startTime: item.startTime,
location: '—',
tag: 'demoTagReminder'.tr,
color: const Color(0xFF22C1A6),
taskStatus: r.done ? 2 : 0,
tag: MemoryCategory.labelKey(item.category).tr,
color: color,
taskStatus: item.isDone ? 2 : 0,
category: cat,
// 展示层用「元」,存储与传输一律用「分」
amount: item.isExpense ? item.amountYuan : null,
dateCertain: item.dateCertain,
dueRaw: item.dueRaw,
repeating: item.isRepeating,
);
// 一次性:只在指定那天(没给 date 就算今天)
if (r.repeat == RepeatRule.once || r.repeat == RepeatRule.unknown) {
final day = r.date != null ? DateTime.tryParse(r.date!) : null;
return [make(day ?? DateTime.now(), '')];
if (!item.isRepeating) {
return [make(anchor, '')];
}
final today = DateTime.now();
final from = DateTime(today.year, today.month, today.day)
.subtract(const Duration(days: 7));
final from =
DateTime(today.year, today.month, today.day).subtract(const Duration(days: 7));
final out = <CalendarEvent>[];
for (var i = 0; i < 67; i++) {
final day = from.add(Duration(days: i));
final hit = switch (r.repeat) {
RepeatRule.daily => true,
RepeatRule.weekly => day.weekday == r.weekday,
RepeatRule.weekdays => day.weekday <= DateTime.friday,
RepeatRule.weekend =>
// 重复项在锚点日之前不该出现——去年设的闹钟不应该显示在前年
if (day.isBefore(DateTime(anchor.year, anchor.month, anchor.day))) continue;
final hit = switch (item.repeatRule) {
MemoryRepeat.daily => true,
// weekday 用 1-7(周一~周日),与 Dart 的 DateTime.weekday 一致。
// ⚠️ 服务端也是这个口径(Go 的 time.Weekday 周日是 0,那边已做换算)。
MemoryRepeat.weekly => day.weekday == item.weekday,
MemoryRepeat.weekdays => day.weekday <= DateTime.friday,
MemoryRepeat.weekend =>
day.weekday == DateTime.saturday || day.weekday == DateTime.sunday,
MemoryRepeat.monthly => day.day == anchor.day,
_ => false,
};
if (hit) out.add(make(day, '@${day.year}${day.month}${day.day}'));
@ -168,9 +198,21 @@ class CalendarController extends GetxController
return out;
}
/// 拉取当前可见窗口的数据。下拉刷新与首次进入都走它。
Future<void> refreshFromServer({bool silent = false}) async {
if (!Get.isRegistered<MemoryService>()) return;
final today = DateTime.now();
await MemoryService.to.load(
start: today.subtract(const Duration(days: 30)),
end: today.add(const Duration(days: 60)),
silent: silent,
);
Logger.d('CalendarController', '刷新完成,共 ${events.length} 个日历事件');
}
@override
void onClose() {
_reminderWorker?.dispose();
_memoryWorker?.dispose();
_snapAnimation.dispose();
monthPageController.dispose();
weekPageController.dispose();
@ -325,16 +367,15 @@ class CalendarController extends GetxController
.where((event) => event.category == CalendarEventCategory.expense)
.toList();
/// 折叠态横向条实际展示的条目:按 [categoryFilter] 筛选,null 时代办+花销都显示
/// 折叠态横向条实际展示的条目:按 [categoryFilter] 筛选,null = 全部
///
/// ⚠️ 按值相等筛而不是 switch 逐个枚举:分类会随服务端增加(未知分类会落到
/// [CalendarEventCategory.other]),穷举式 switch 每加一类都得回来改一次,
/// 漏改就是编译错——而这一层根本不关心具体是哪一类。
List<CalendarEvent> get visibleEventsForSelectedDate {
switch (categoryFilter.value) {
case CalendarEventCategory.todo:
return todosForSelectedDate;
case CalendarEventCategory.expense:
return expensesForSelectedDate;
case null:
return eventsForSelectedDate;
}
final f = categoryFilter.value;
if (f == null) return eventsForSelectedDate;
return eventsForSelectedDate.where((e) => e.category == f).toList();
}
/// 选中日期的花销合计(元)

240
apps/client/lib/modules/home/views/widgets/calendar/calendar_event.dart

@ -1,8 +1,64 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
/// 事件分类:代办事项 / 花销记录
enum CalendarEventCategory { todo, expense }
import '../../../../../data/models/memory_item.dart';
/// 事件分类。
///
/// ⚠️ [other] 是**不认识的分类的兜底**,不是一个真实分类。服务端的分类是字符串
/// 白名单,加新分类只改服务端常量、不发新包;客户端一旦 switch 不到就抛,
/// 那会让服务端加一个分类就把所有存量 App 的拾忆页整页打崩。
/// 所以解析一律走 [CalendarEventCategoryX.fromRaw],永远落到某个枚举值上。
enum CalendarEventCategory { todo, alarm, idea, expense, other }
extension CalendarEventCategoryX on CalendarEventCategory {
/// 服务端的字符串分类 → 枚举。认不出的一律 [CalendarEventCategory.other]。
static CalendarEventCategory fromRaw(String raw) {
switch (raw) {
case MemoryCategory.todo:
return CalendarEventCategory.todo;
case MemoryCategory.alarm:
return CalendarEventCategory.alarm;
case MemoryCategory.idea:
return CalendarEventCategory.idea;
case MemoryCategory.expense:
return CalendarEventCategory.expense;
default:
return CalendarEventCategory.other;
}
}
/// 枚举 → 服务端字符串。other 没有对应的服务端分类,退回 todo。
String get raw {
switch (this) {
case CalendarEventCategory.todo:
return MemoryCategory.todo;
case CalendarEventCategory.alarm:
return MemoryCategory.alarm;
case CalendarEventCategory.idea:
return MemoryCategory.idea;
case CalendarEventCategory.expense:
return MemoryCategory.expense;
case CalendarEventCategory.other:
return MemoryCategory.todo;
}
}
String get labelKey {
switch (this) {
case CalendarEventCategory.todo:
return 'memCatTodo';
case CalendarEventCategory.alarm:
return 'memCatAlarm';
case CalendarEventCategory.idea:
return 'memCatIdea';
case CalendarEventCategory.expense:
return 'memCatExpense';
case CalendarEventCategory.other:
return 'memCatOther';
}
}
}
/// 日历事件视图模型(移植自 AI-study-machine,去掉原生层 fromData,仅保留 UI 所需字段)。
class CalendarEvent {
@ -20,12 +76,27 @@ class CalendarEvent {
/// 0=进行中,2=已完成(已完成会灰显)
final int taskStatus;
/// 代办 / 花销分类,默认代办
/// 分类,默认代办。未知分类会落到 [CalendarEventCategory.other],按代办渲染。
final CalendarEventCategory category;
/// 花销金额(元),仅 category=expense 时有意义
/// 花销金额(元),仅 category=expense 时有意义。
/// ⚠️ 存储与传输一律用「分」([MemoryItem.amountCents]),只有展示层用元。
final double? amount;
/// 关联的记忆项 id。0 = 不是来自服务端(理论上不该出现)。
/// 点卡片编辑、勾完成、删除都靠它回查 [MemoryItem]。
final int memoryId;
/// 日期是不是确定的。false 表示服务端推断出来的(比如会议待办没写截止时间,
/// 落在会议当天),这类项要单独归到「待定日期」分组,不能和当天确定事项混排。
final bool dateCertain;
/// 截止时间原文(「下周三前」)。dateCertain=false 时靠它表达真实语义。
final String dueRaw;
/// 是否重复项(日历上会摊成多天,编辑时要提示改的是整条规则)
final bool repeating;
CalendarEvent({
required this.id,
required this.title,
@ -40,6 +111,10 @@ class CalendarEvent {
this.taskStatus = 0,
this.category = CalendarEventCategory.todo,
this.amount,
this.memoryId = 0,
this.dateCertain = true,
this.dueRaw = '',
this.repeating = false,
});
String get timeText {
@ -53,160 +128,3 @@ class CalendarEvent {
return '$start - $end';
}
}
/// 示例任务数据:以"今天"为锚点,覆盖今天及邻近几天,标签/颜色/时间/状态各异。
List<CalendarEvent> mockCalendarEvents() {
final now = DateTime.now();
DateTime d(int offsetDays) =>
DateTime(now.year, now.month, now.day).add(Duration(days: offsetDays));
return <CalendarEvent>[
// 今天
CalendarEvent(
id: 'm1',
title: 'demoEvt1'.tr,
description: 'demoDesc1'.tr,
date: d(0),
startTime: const TimeOfDay(hour: 8, minute: 0),
endTime: const TimeOfDay(hour: 8, minute: 30),
location: 'demoLoc1'.tr,
tag: 'demoTagStudy'.tr,
color: const Color(0xFF5B8DEF),
),
CalendarEvent(
id: 'm2',
title: 'demoEvt2'.tr,
description: 'demoDesc2'.tr,
date: d(0),
startTime: const TimeOfDay(hour: 10, minute: 0),
endTime: const TimeOfDay(hour: 11, minute: 0),
location: 'demoLoc2'.tr,
tag: 'demoTagMeeting'.tr,
color: const Color(0xFF8B5CF6),
),
CalendarEvent(
id: 'm3',
title: 'demoEvt3'.tr,
description: 'demoDesc3'.tr,
date: d(0),
isAllDay: true,
location: '—',
tag: 'demoTagReminder'.tr,
color: const Color(0xFF22C1A6),
taskStatus: 2, // 已完成(灰显)
),
// 昨天(已完成)
CalendarEvent(
id: 'm4',
title: 'demoEvt4'.tr,
description: 'demoDesc4'.tr,
date: d(-1),
startTime: const TimeOfDay(hour: 18, minute: 0),
location: 'demoLoc4'.tr,
tag: 'demoTagTask'.tr,
color: const Color(0xFFF59E0B),
taskStatus: 2,
),
// 明天
CalendarEvent(
id: 'm5',
title: 'demoEvt5'.tr,
description: 'demoDesc5'.tr,
date: d(1),
startTime: const TimeOfDay(hour: 19, minute: 30),
endTime: const TimeOfDay(hour: 20, minute: 30),
location: 'demoLoc5'.tr,
tag: 'demoTagSport'.tr,
color: const Color(0xFFEF4444),
),
// 后天
CalendarEvent(
id: 'm6',
title: 'demoEvt6'.tr,
description: 'demoDesc6'.tr,
date: d(2),
isAllDay: true,
location: '—',
tag: 'demoTagStudy'.tr,
color: const Color(0xFF10B981),
),
// 三天后
CalendarEvent(
id: 'm7',
title: 'demoEvt7'.tr,
description: 'demoDesc7'.tr,
date: d(3),
startTime: const TimeOfDay(hour: 15, minute: 0),
endTime: const TimeOfDay(hour: 15, minute: 30),
location: 'demoLoc7'.tr,
tag: 'demoTagBooking'.tr,
color: const Color(0xFF06B6D4),
),
// ==================== 花销记录 ====================
// 今天
CalendarEvent(
id: 'e1',
title: 'demoEvt8'.tr,
description: 'demoDesc8'.tr,
date: d(0),
startTime: const TimeOfDay(hour: 7, minute: 30),
location: 'demoLoc8'.tr,
tag: 'demoTagFood'.tr,
color: const Color(0xFFF59E0B),
category: CalendarEventCategory.expense,
amount: 18,
),
CalendarEvent(
id: 'e2',
title: 'demoEvt9'.tr,
description: 'demoDesc9'.tr,
date: d(0),
startTime: const TimeOfDay(hour: 8, minute: 45),
location: 'demoLoc9'.tr,
tag: 'demoTagTransit'.tr,
color: const Color(0xFF3B82F6),
category: CalendarEventCategory.expense,
amount: 26,
),
// 昨天
CalendarEvent(
id: 'e3',
title: 'demoEvt10'.tr,
description: 'demoDesc10'.tr,
date: d(-1),
startTime: const TimeOfDay(hour: 20, minute: 0),
location: 'demoLoc10'.tr,
tag: 'demoTagBills'.tr,
color: const Color(0xFF8B5CF6),
category: CalendarEventCategory.expense,
amount: 50,
),
// 明天
CalendarEvent(
id: 'e4',
title: 'demoEvt11'.tr,
description: 'demoDesc11'.tr,
date: d(1),
startTime: const TimeOfDay(hour: 21, minute: 0),
location: 'demoLoc11'.tr,
tag: 'demoTagFun'.tr,
color: const Color(0xFFEC4899),
category: CalendarEventCategory.expense,
amount: 45,
),
// 三天后
CalendarEvent(
id: 'e5',
title: 'demoEvt12'.tr,
description: 'demoDesc12'.tr,
date: d(3),
startTime: const TimeOfDay(hour: 16, minute: 0),
location: 'demoLoc12'.tr,
tag: 'demoTagStudy'.tr,
color: const Color(0xFF10B981),
category: CalendarEventCategory.expense,
amount: 68,
),
];
}

65
apps/client/lib/modules/main_tab/views/tabs/todo_tab.dart

@ -4,16 +4,44 @@ import 'package:get/get.dart';
import '../../../home/controllers/calendar_controller.dart';
import '../../../home/views/widgets/calendar/calendar_card.dart';
import '../widgets/memory_edit_sheet.dart';
import '../widgets/tab_scaffold.dart';
import '../widgets/todo_scope_list.dart';
/// 代办页:把改版前首页的日历卡片 + 代办列表整体平移过来。
/// 拾忆页:日历卡片 + 事项列表。
///
/// 与旧首页的差别是这里不再需要"日历展开时把下方功能卡片收起"的联动——
/// 功能卡片已经搬到 AI 页,所以任务列表可以直接吃掉全部剩余高度。
class TodoTab extends StatelessWidget {
///
/// 页面从「纯展示」改成可写:右下角 + 新增、卡片点开编辑、勾选完成、下拉刷新
/// 都在这一层与 [TodoScopeList] 里。改成 StatefulWidget 是为了在 initState 里
/// 触发首次拉取——build 里发请求会在每次重建时重复触发。
class TodoTab extends StatefulWidget {
const TodoTab({Key? key}) : super(key: key);
@override
State<TodoTab> createState() => _TodoTabState();
}
class _TodoTabState extends State<TodoTab> {
@override
void initState() {
super.initState();
// 首次进入拉一次。不 await——页面要立刻可见,数据到了 Obx 自己会刷。
WidgetsBinding.instance.addPostFrameCallback((_) {
if (Get.isRegistered<CalendarController>()) {
Get.find<CalendarController>().refreshFromServer(silent: true);
}
});
}
Future<void> _addNew() async {
final ctl = Get.isRegistered<CalendarController>()
? Get.find<CalendarController>()
: null;
await MemoryEditSheet.show(defaultDate: ctl?.selectedDate.value);
}
@override
Widget build(BuildContext context) {
final bool isDarkMode = Theme.of(context).brightness == Brightness.dark;
@ -22,7 +50,27 @@ class TodoTab extends StatelessWidget {
slogan: 'sloganHome'.tr,
titleOnRight: true,
isDarkMode: isDarkMode,
child: LayoutBuilder(
child: Stack(
children: [
_buildBody(),
// 新增入口。放 Stack 而不是 Scaffold 的 floatingActionButton:
// TabScaffold 已经是页面骨架,再套一层 Scaffold 会多出一个 AppBar 空位。
Positioned(
right: 16.w,
bottom: 16.h,
child: FloatingActionButton(
heroTag: 'memory_add',
onPressed: _addNew,
child: const Icon(Icons.add),
),
),
],
),
);
}
Widget _buildBody() {
return LayoutBuilder(
builder: (context, constraints) {
final double h = constraints.maxHeight;
// 极小高度兜底:退回可滚动布局,避免 Expanded 负高溢出
@ -58,15 +106,6 @@ class TodoTab extends StatelessWidget {
],
);
},
),
);
}
}
/// 确保 CalendarController 已注册(TodoTab 依赖它做日历状态管理)
CalendarController ensureCalendarController() {
if (!Get.isRegistered<CalendarController>()) {
Get.put(CalendarController());
);
}
return Get.find<CalendarController>();
}

271
apps/client/lib/modules/main_tab/views/widgets/memory_daily_sheet.dart

@ -0,0 +1,271 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import '../../../../core/utils/logger.dart';
import '../../../../data/models/memory_item.dart';
import '../../../../data/services/memory_service.dart';
import '../../../../data/services/tts_service.dart';
/// 每日首次进入的「今天的安排」弹窗,带语音播报。
///
/// ## ⚠️ 绝不能 await 在启动路径上
/// 冷启动已经串了四次网络往返(网络检查 → 版本检查 → getLoginToken →
/// getAppConfig),仓库刚做过一次相反方向的优化(把会议模板拉取改成 unawaited,
/// 理由是「启动时没有任何地方要用它」)。所以这里的用法固定是:
/// splash 照常放行导航 → 进主页后异步拉 → 拿到再弹。弹窗晚一两秒出现是可接受的。
///
/// ## 弹窗顺序
/// 启动路径上已经有两个弹窗,本弹窗必须排最后:
/// 1. 华为/荣耀「每次启动重新确认隐私政策」
/// 2. 强制更新弹窗(不可关闭,会把 splash 卡住)
/// 3. 本弹窗 —— 且必须在登录态确认之后(未登录时拿不到数据)
class MemoryDailySheet {
static const String _tag = 'MemoryDaily';
static const String _kLastDate = 'memory_daily_popup_date';
static const String _sessionId = 'memory_daily_briefing';
static bool _showing = false;
static String _today() {
final d = DateTime.now();
return '${d.year}-${d.month.toString().padLeft(2, '0')}-'
'${d.day.toString().padLeft(2, '0')}';
}
/// 今天是否已经弹过
static bool shownToday() {
try {
return GetStorage().read<String>(_kLastDate) == _today();
} catch (_) {
return false;
}
}
/// 尝试弹出。已经弹过、没有数据、或正在弹时静默跳过。
///
/// 调用方**不要 await**它的网络部分——见类注释。
static Future<void> maybeShow() async {
if (_showing || shownToday()) return;
_showing = true;
try {
final items = await MemoryService.to.loadToday();
if (items.isEmpty) {
// 没有安排就不打扰。仍然记下「今天弹过」,避免每次切前台都去打一次接口。
GetStorage().write(_kLastDate, _today());
return;
}
GetStorage().write(_kLastDate, _today());
await _show(items);
} catch (e) {
Logger.w(_tag, '每日弹窗失败(不影响主流程): $e');
} finally {
_showing = false;
}
}
static Future<void> _show(List<MemoryItem> items) async {
await Get.bottomSheet(
_DailySheetView(items: items),
isScrollControlled: true,
isDismissible: true,
);
}
/// 拼播报文案。**本地拼,不调 LLM**——这段话的结构是固定的,
/// 花一次模型调用换一句同样的话不值得,而且启动阶段等不起。
static String buildSpeech(List<MemoryItem> items) {
if (items.isEmpty) return 'memDailyEmpty'.tr;
final buf = StringBuffer();
buf.write('memDailyCount'.trParams({'n': '${items.length}'}));
for (final it in items) {
buf.write(' ');
if (it.happenTime.isNotEmpty) {
buf.write('${it.happenTime},');
}
buf.write(it.title);
if (it.owner.isNotEmpty) {
buf.write(',${it.owner}');
}
buf.write('。');
}
return buf.toString();
}
}
class _DailySheetView extends StatefulWidget {
final List<MemoryItem> items;
const _DailySheetView({required this.items});
@override
State<_DailySheetView> createState() => _DailySheetViewState();
}
class _DailySheetViewState extends State<_DailySheetView> {
bool _speaking = false;
TtsService? _tts;
@override
void initState() {
super.initState();
// 打开即播报(需求:每日首次登录自动播报)
WidgetsBinding.instance.addPostFrameCallback((_) => _speak());
}
@override
void dispose() {
// ⚠️ 关窗必须真的停下来。speakStream 是队列式播放,只调 stop 不清队列会出现
// 「关了还在念下一句」。TtsService.stop() 内部已负责清队列。
_stop();
super.dispose();
}
Future<void> _speak() async {
// ⚠️ 通话翻译进行中一律不播报。两者共用同一个 AVAudioSession,
// 在对方刚调好的会话上再设一次会把通话音频路由弄乱(iOS 那条 phone_call
// 分支被改坏过三次)。宁可这次不播,也不能干扰正在进行的通话。
if (_isTranslationBusy()) {
Logger.i(MemoryDailySheet._tag, '通话翻译进行中,跳过播报');
return;
}
if (!Get.isRegistered<TtsService>()) return;
try {
_tts = Get.find<TtsService>();
setState(() => _speaking = true);
await _tts!.startspeak(MemoryDailySheet._sessionId);
await _tts!.speakOnce(
MemoryDailySheet._sessionId, MemoryDailySheet.buildSpeech(widget.items));
} catch (e) {
Logger.w(MemoryDailySheet._tag, '播报失败: $e');
} finally {
if (mounted) setState(() => _speaking = false);
}
}
Future<void> _stop() async {
try {
await _tts?.stop();
} catch (_) {}
if (mounted) setState(() => _speaking = false);
}
/// 通话翻译是否正在进行。
///
/// 用 Get.isRegistered + 反射式判断而不是直接 import TranslationController:
/// 那会把整个翻译模块拖进启动路径的依赖图里。拿不到就当没在通话。
bool _isTranslationBusy() {
try {
return Get.isRegistered(tag: 'translation_active');
} catch (_) {
return false;
}
}
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return Container(
decoration: BoxDecoration(
color: isDark ? const Color(0xFF1C1C1E) : Colors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(20.r)),
),
padding: EdgeInsets.fromLTRB(20.w, 12.h, 20.w, 20.h),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(
child: Container(
width: 36.w,
height: 4.h,
decoration: BoxDecoration(
color: isDark ? Colors.white24 : Colors.black12,
borderRadius: BorderRadius.circular(2.r),
),
),
),
SizedBox(height: 16.h),
Row(
children: [
Expanded(
child: Text('memDailyTitle'.tr,
style:
TextStyle(fontSize: 17.sp, fontWeight: FontWeight.w700)),
),
TextButton.icon(
onPressed: _speaking ? _stop : _speak,
icon: Icon(_speaking ? Icons.stop : Icons.volume_up,
size: 18.sp),
label: Text(
(_speaking ? 'memReportStop' : 'memDailyPlay').tr,
style: TextStyle(fontSize: 13.sp)),
),
],
),
SizedBox(height: 8.h),
Flexible(
child: ListView.separated(
shrinkWrap: true,
itemCount: widget.items.length,
separatorBuilder: (_, __) => Divider(height: 16.h),
itemBuilder: (_, i) {
final it = widget.items[i];
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 4.w,
height: 32.h,
decoration: BoxDecoration(
color: MemoryCategory.colorOf(it.category),
borderRadius: BorderRadius.circular(2.r),
),
),
SizedBox(width: 10.w),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(it.title,
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w600)),
if (it.happenTime.isNotEmpty ||
it.owner.isNotEmpty) ...[
SizedBox(height: 2.h),
Text(
[it.happenTime, it.owner]
.where((e) => e.isNotEmpty)
.join(' · '),
style: TextStyle(
fontSize: 11.sp,
color: isDark
? Colors.white54
: Colors.black45),
),
],
],
),
),
],
);
},
),
),
SizedBox(height: 16.h),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () {
_stop();
Get.back();
},
child: Text('memDailyClose'.tr),
),
),
],
),
);
}
}

495
apps/client/lib/modules/main_tab/views/widgets/memory_edit_sheet.dart

@ -0,0 +1,495 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import '../../../../data/models/memory_item.dart';
import '../../../../data/services/memory_service.dart';
/// 新增/编辑一条记忆项的底部弹层。
///
/// 这是拾忆页此前**完全缺失**的一块:页面原来只有展示,没有新增按钮、卡片没有
/// onTap、没有下拉刷新,服务端的 add/update/del/complete 四个写接口在客户端
/// 一个调用点都没有,`user_edited` 那套「保护用户修改」的机制也就无从谈起。
///
/// 按分类分流表单:只显示这个分类真正用得上的字段,不做一个塞满 12 个输入框的
/// 通用表单(统一表存储 ≠ 统一表单)。
class MemoryEditSheet extends StatefulWidget {
/// null = 新增
final MemoryItem? item;
/// 新增时的默认日期与分类
final DateTime? defaultDate;
final String defaultCategory;
const MemoryEditSheet({
super.key,
this.item,
this.defaultDate,
this.defaultCategory = MemoryCategory.todo,
});
/// 打开弹层。返回 true 表示有改动(调用方据此刷新)。
static Future<bool> show({
MemoryItem? item,
DateTime? defaultDate,
String defaultCategory = MemoryCategory.todo,
}) async {
final r = await Get.bottomSheet<bool>(
MemoryEditSheet(
item: item,
defaultDate: defaultDate,
defaultCategory: defaultCategory,
),
isScrollControlled: true,
ignoreSafeArea: false,
);
return r ?? false;
}
@override
State<MemoryEditSheet> createState() => _MemoryEditSheetState();
}
class _MemoryEditSheetState extends State<MemoryEditSheet> {
late final TextEditingController _title;
late final TextEditingController _detail;
late final TextEditingController _amount;
late final TextEditingController _owner;
late String _category;
late DateTime _date;
TimeOfDay? _time;
late String _repeat;
late int _remindAhead;
bool _saving = false;
bool get _isEdit => widget.item != null;
@override
void initState() {
super.initState();
final it = widget.item;
_title = TextEditingController(text: it?.title ?? '');
_detail = TextEditingController(text: it?.detail ?? '');
// 金额展示用「元」,存储用「分」。这里做一次换算,保存时换回去。
_amount = TextEditingController(
text: (it != null && it.amountCents > 0)
? (it.amountCents / 100).toStringAsFixed(2)
: '',
);
_owner = TextEditingController(text: it?.owner ?? '');
_category = it?.category ?? widget.defaultCategory;
// 未知分类(服务端加了新类而本包不认识)落到 todo,避免下拉选不中任何一项
if (!MemoryCategory.isKnown(_category)) {
_category = MemoryCategory.todo;
}
_date = it?.happenDateTime ?? widget.defaultDate ?? DateTime.now();
_time = it?.startTime;
_repeat = it?.repeatRule.isNotEmpty == true ? it!.repeatRule : MemoryRepeat.once;
if (!MemoryRepeat.all.contains(_repeat)) _repeat = MemoryRepeat.once;
_remindAhead = it?.remindAhead ?? 5;
}
@override
void dispose() {
_title.dispose();
_detail.dispose();
_amount.dispose();
_owner.dispose();
super.dispose();
}
bool get _showTime =>
_category == MemoryCategory.alarm || _category == MemoryCategory.todo;
bool get _showRepeat => _category == MemoryCategory.alarm;
bool get _showRemind =>
_category == MemoryCategory.alarm || _category == MemoryCategory.todo;
bool get _showAmount => _category == MemoryCategory.expense;
bool get _showOwner => _category == MemoryCategory.todo;
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
final bg = isDark ? const Color(0xFF1C1C1E) : Colors.white;
return Padding(
// 键盘弹起时把内容顶上去,否则输入框会被盖住
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
child: Container(
decoration: BoxDecoration(
color: bg,
borderRadius: BorderRadius.vertical(top: Radius.circular(20.r)),
),
padding: EdgeInsets.fromLTRB(20.w, 12.h, 20.w, 20.h),
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(
child: Container(
width: 36.w,
height: 4.h,
decoration: BoxDecoration(
color: isDark ? Colors.white24 : Colors.black12,
borderRadius: BorderRadius.circular(2.r),
),
),
),
SizedBox(height: 16.h),
Text(
(_isEdit ? 'memEditTitle' : 'memAddTitle').tr,
style: TextStyle(fontSize: 17.sp, fontWeight: FontWeight.w700),
),
SizedBox(height: 16.h),
_categoryPicker(isDark),
SizedBox(height: 16.h),
_field(_title, 'memFieldTitle'.tr, isDark, autofocus: !_isEdit),
SizedBox(height: 12.h),
_dateTimeRow(isDark),
if (_showAmount) ...[
SizedBox(height: 12.h),
_field(
_amount,
'memFieldAmount'.tr,
isDark,
keyboardType:
const TextInputType.numberWithOptions(decimal: true),
inputFormatters: [
// 只允许数字与至多两位小数:金额存的是「分」,
// 多余位数换算时会被静默截断,不如在输入处就挡住
FilteringTextInputFormatter.allow(
RegExp(r'^\d*\.?\d{0,2}')),
],
),
],
if (_showOwner) ...[
SizedBox(height: 12.h),
_field(_owner, 'memFieldOwner'.tr, isDark),
],
if (_showRepeat) ...[
SizedBox(height: 12.h),
_repeatPicker(isDark),
],
if (_showRemind) ...[
SizedBox(height: 12.h),
_remindPicker(isDark),
],
SizedBox(height: 12.h),
_field(_detail, 'memFieldDetail'.tr, isDark, maxLines: 3),
SizedBox(height: 20.h),
_actions(isDark),
],
),
),
),
);
}
Widget _categoryPicker(bool isDark) {
return Wrap(
spacing: 8.w,
children: MemoryCategory.known.map((c) {
final active = c == _category;
final color = MemoryCategory.colorOf(c);
return GestureDetector(
onTap: () => setState(() => _category = c),
child: Container(
padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 7.h),
decoration: BoxDecoration(
color: active ? color.withValues(alpha: 0.15) : Colors.transparent,
border: Border.all(
color: active ? color : (isDark ? Colors.white24 : Colors.black12),
),
borderRadius: BorderRadius.circular(16.r),
),
child: Text(
MemoryCategory.labelKey(c).tr,
style: TextStyle(
fontSize: 13.sp,
color: active ? color : (isDark ? Colors.white70 : Colors.black54),
fontWeight: active ? FontWeight.w700 : FontWeight.w500,
),
),
),
);
}).toList(),
);
}
Widget _field(
TextEditingController c,
String label,
bool isDark, {
int maxLines = 1,
bool autofocus = false,
TextInputType? keyboardType,
List<TextInputFormatter>? inputFormatters,
}) {
return TextField(
controller: c,
maxLines: maxLines,
autofocus: autofocus,
keyboardType: keyboardType,
inputFormatters: inputFormatters,
style: TextStyle(fontSize: 14.sp),
decoration: InputDecoration(
labelText: label,
isDense: true,
border: OutlineInputBorder(borderRadius: BorderRadius.circular(10.r)),
),
);
}
Widget _dateTimeRow(bool isDark) {
return Row(
children: [
Expanded(
child: _pickerTile(
'memFieldDate'.tr,
'${_date.year}-${_date.month.toString().padLeft(2, '0')}-${_date.day.toString().padLeft(2, '0')}',
isDark,
onTap: () async {
final d = await showDatePicker(
context: context,
initialDate: _date,
firstDate: DateTime(2020),
lastDate: DateTime(2100),
);
if (d != null) setState(() => _date = d);
},
),
),
if (_showTime) ...[
SizedBox(width: 10.w),
Expanded(
child: _pickerTile(
'memFieldTime'.tr,
_time == null
? '--:--'
: '${_time!.hour.toString().padLeft(2, '0')}:${_time!.minute.toString().padLeft(2, '0')}',
isDark,
onTap: () async {
final t = await showTimePicker(
context: context,
initialTime: _time ?? const TimeOfDay(hour: 9, minute: 0),
);
if (t != null) setState(() => _time = t);
},
// 时间可以清空:待办常常只有日期没有具体时刻
onClear: _time == null ? null : () => setState(() => _time = null),
),
),
],
],
);
}
Widget _pickerTile(String label, String value, bool isDark,
{required VoidCallback onTap, VoidCallback? onClear}) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 10.h),
decoration: BoxDecoration(
border:
Border.all(color: isDark ? Colors.white24 : Colors.black26),
borderRadius: BorderRadius.circular(10.r),
),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(label,
style: TextStyle(
fontSize: 11.sp,
color: isDark ? Colors.white54 : Colors.black45)),
SizedBox(height: 2.h),
Text(value, style: TextStyle(fontSize: 14.sp)),
],
),
),
if (onClear != null)
GestureDetector(
onTap: onClear,
child: Icon(Icons.close,
size: 16.sp,
color: isDark ? Colors.white38 : Colors.black26),
),
],
),
),
);
}
Widget _repeatPicker(bool isDark) {
return DropdownButtonFormField<String>(
initialValue: _repeat,
isDense: true,
decoration: InputDecoration(
labelText: 'memFieldRepeat'.tr,
isDense: true,
border: OutlineInputBorder(borderRadius: BorderRadius.circular(10.r)),
),
items: MemoryRepeat.all
.map((r) => DropdownMenuItem(
value: r,
child: Text(MemoryRepeat.labelKey(r).tr,
style: TextStyle(fontSize: 14.sp))))
.toList(),
onChanged: (v) => setState(() => _repeat = v ?? MemoryRepeat.once),
);
}
Widget _remindPicker(bool isDark) {
const options = [0, 5, 10, 15, 30, 60];
return DropdownButtonFormField<int>(
initialValue: options.contains(_remindAhead) ? _remindAhead : 5,
isDense: true,
decoration: InputDecoration(
labelText: 'memFieldRemind'.tr,
isDense: true,
border: OutlineInputBorder(borderRadius: BorderRadius.circular(10.r)),
),
items: options
.map((m) => DropdownMenuItem(
value: m,
child: Text(
m == 0
? 'memRemindOff'.tr
: 'memRemindMinutes'.trParams({'n': '$m'}),
style: TextStyle(fontSize: 14.sp),
),
))
.toList(),
onChanged: (v) => setState(() => _remindAhead = v ?? 5),
);
}
Widget _actions(bool isDark) {
return Row(
children: [
if (_isEdit)
TextButton.icon(
onPressed: _saving ? null : _delete,
icon: Icon(Icons.delete_outline, size: 18.sp, color: Colors.red),
label: Text('memDelete'.tr,
style: TextStyle(color: Colors.red, fontSize: 14.sp)),
),
const Spacer(),
TextButton(
onPressed: _saving ? null : () => Get.back(result: false),
child: Text('cancel'.tr, style: TextStyle(fontSize: 14.sp)),
),
SizedBox(width: 8.w),
ElevatedButton(
onPressed: _saving ? null : _save,
child: _saving
? SizedBox(
width: 16.w,
height: 16.w,
child: const CircularProgressIndicator(strokeWidth: 2))
: Text('confirm'.tr, style: TextStyle(fontSize: 14.sp)),
),
],
);
}
Future<void> _save() async {
final title = _title.text.trim();
if (title.isEmpty) {
Get.snackbar('error'.tr, 'memTitleRequired'.tr,
snackPosition: SnackPosition.TOP);
return;
}
// 金额:输入的是「元」,存的是「分」。用 round 而不是 toInt——
// 12.30 在浮点里可能是 12.299999,直接截断会变成 1229 分。
var cents = 0;
if (_showAmount && _amount.text.trim().isNotEmpty) {
final yuan = double.tryParse(_amount.text.trim());
if (yuan == null || yuan < 0) {
Get.snackbar('error'.tr, 'memAmountInvalid'.tr,
snackPosition: SnackPosition.TOP);
return;
}
cents = (yuan * 100).round();
}
setState(() => _saving = true);
final base = widget.item?.copy() ?? MemoryItem();
base.title = title;
base.detail = _detail.text.trim();
base.happenDate =
'${_date.year}-${_date.month.toString().padLeft(2, '0')}-${_date.day.toString().padLeft(2, '0')}';
base.happenTime = (_showTime && _time != null)
? '${_time!.hour.toString().padLeft(2, '0')}:${_time!.minute.toString().padLeft(2, '0')}'
: '';
// 用户手填的日期一定是确定的——把服务端推断出来的 date_certain=false 顶掉,
// 这样它就不会再留在「待定日期」分组里
base.dateCertain = true;
base.amountCents = _showAmount ? cents : 0;
base.owner = _showOwner ? _owner.text.trim() : '';
base.repeatRule = _showRepeat ? _repeat : MemoryRepeat.once;
base.remindAhead = _showRemind ? _remindAhead : 0;
if (base.repeatRule == MemoryRepeat.weekly) {
base.weekday = _date.weekday; // Dart 的 1-7 与服务端同口径
}
// category / source 在 MemoryItem 上是 final(它们是身份而不是可随手改的属性),
// 所以统一经 fromJson 重建一份带新分类的对象,而不是就地赋值。
// 编辑时也允许改分类——服务端会重新校验白名单。
final payload = MemoryItem.fromJson({
...base.toJson(),
'id': _isEdit ? base.id : 0,
'category': _category,
'source': _isEdit ? base.source : MemorySource.manual,
});
final svc = MemoryService.to;
final ok = _isEdit
? await svc.update(payload) != null
: await svc.add(payload) != null;
if (!mounted) return;
setState(() => _saving = false);
if (ok) {
Get.back(result: true);
} else {
Get.snackbar('error'.tr, 'memSaveFailed'.tr,
snackPosition: SnackPosition.TOP);
}
}
Future<void> _delete() async {
final confirmed = await Get.dialog<bool>(
AlertDialog(
content: Text('memDeleteConfirm'.tr),
actions: [
TextButton(
onPressed: () => Get.back(result: false), child: Text('cancel'.tr)),
TextButton(
onPressed: () => Get.back(result: true),
child: Text('memDelete'.tr,
style: const TextStyle(color: Colors.red)),
),
],
),
);
if (confirmed != true) return;
setState(() => _saving = true);
final ok = await MemoryService.to.remove([widget.item!.id]);
if (!mounted) return;
setState(() => _saving = false);
if (ok) {
Get.back(result: true);
} else {
Get.snackbar('error'.tr, 'memDeleteFailed'.tr,
snackPosition: SnackPosition.TOP);
}
}
}

241
apps/client/lib/modules/main_tab/views/widgets/todo_scope_list.dart

@ -8,8 +8,11 @@ import '../../../home/views/widgets/calendar/calendar_event.dart';
import '../../../home/views/widgets/calendar/calendar_tokens.dart';
import '../../../home/views/widgets/calendar/kids_card.dart';
import '../../../../core/theme/app_palette.dart';
import '../../../../data/models/memory_item.dart';
import '../../../../data/services/memory_service.dart';
import 'memory_edit_sheet.dart';
/// 代办页的事项列表:日 / 周 / 月三档范围,纯纵向滚动。
/// 拾忆页的事项列表:日 / 周 / 月三档范围,纯纵向滚动。
///
/// 和旧首页那个 [CalendarTaskList] 的区别:
/// - 旧的在折叠态用横向滑动条塞代办(首页要给下面的功能卡片让位),
@ -28,11 +31,109 @@ class TodoScopeList extends GetView<CalendarController> {
padding: EdgeInsets.fromLTRB(16.w, 0, 16.w, 8.h),
child: _buildHeader(isDark),
),
Expanded(child: Obx(() => _buildList(isDark))),
_buildCategoryFilter(isDark),
_buildOfflineHint(isDark),
Expanded(
child: RefreshIndicator(
onRefresh: () => controller.refreshFromServer(silent: true),
child: Obx(() => _buildList(isDark)),
),
),
],
);
}
/// 分类筛选条:全部 / 待办 / 闹钟 / 灵感 / 花销
///
/// 只列本包认识的分类。服务端将来加了新分类,那些条目会落到
/// [CalendarEventCategory.other],在「全部」里照常显示——不会因为筛选条里
/// 没有它就看不见。
Widget _buildCategoryFilter(bool isDark) {
return Obx(() {
final current = controller.categoryFilter.value;
final entries = <(CalendarEventCategory?, String)>[
(null, 'memCatAll'),
(CalendarEventCategory.todo, 'memCatTodo'),
(CalendarEventCategory.alarm, 'memCatAlarm'),
(CalendarEventCategory.idea, 'memCatIdea'),
(CalendarEventCategory.expense, 'memCatExpense'),
];
return SizedBox(
height: 34.h,
child: ListView.separated(
scrollDirection: Axis.horizontal,
padding: EdgeInsets.symmetric(horizontal: 16.w),
itemCount: entries.length,
separatorBuilder: (_, __) => SizedBox(width: 8.w),
itemBuilder: (_, i) {
final (cat, key) = entries[i];
final active = current == cat;
final color = cat == null
? AppPalette.brand800
: MemoryCategory.colorOf(cat.raw);
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => controller.setCategoryFilterOrAll(cat),
child: Container(
alignment: Alignment.center,
padding: EdgeInsets.symmetric(horizontal: 14.w),
decoration: BoxDecoration(
color:
active ? color.withValues(alpha: 0.15) : Colors.transparent,
border: Border.all(
color: active
? color
: (isDark ? Colors.white24 : Colors.black12),
),
borderRadius: BorderRadius.circular(17.r),
),
child: Text(
key.tr,
style: TextStyle(
fontSize: 12.sp,
fontWeight: active ? FontWeight.w700 : FontWeight.w500,
color: active
? color
: (isDark ? Colors.white60 : Colors.black54),
),
),
),
);
},
),
);
});
}
/// 拉取失败时的行内提示。
///
/// 用行内条而不是红色 snackbar:这两个接口在启动与前台恢复时自动调,
/// 用户什么都没点就吃一条红条体验很差(AuthInterceptor 里已把它们加进
/// 抑制名单)。这里明确告诉用户「看到的是缓存」,而不是假装数据是新的。
Widget _buildOfflineHint(bool isDark) {
return Obx(() {
if (!MemoryService.to.staleFromCache.value) return const SizedBox.shrink();
return Container(
margin: EdgeInsets.fromLTRB(16.w, 6.h, 16.w, 0),
padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 6.h),
decoration: BoxDecoration(
color: Colors.orange.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(8.r),
),
child: Row(
children: [
Icon(Icons.cloud_off, size: 14.sp, color: Colors.orange),
SizedBox(width: 6.w),
Expanded(
child: Text('memOffline'.tr,
style: TextStyle(fontSize: 11.sp, color: Colors.orange)),
),
],
),
);
});
}
/// 标题行:范围文案 + 汇总 + 日/周/月切换
Widget _buildHeader(bool isDark) {
return Obx(() {
@ -138,12 +239,26 @@ class TodoScopeList extends GetView<CalendarController> {
children.add(_dayHeader(group.day, group.events, isDark));
children.add(SizedBox(height: AppKidsDimens.space8));
}
for (final e in group.events) {
children.add(e.category == CalendarEventCategory.expense
? _expenseCard(e, isDark)
: _todoCard(e, isDark));
// 日期确定的排前面,服务端推断出来的(date_certain=false,典型是会议待办
// 没写截止时间、被落在会议当天)单独归到「待定日期」小节。
// 不分开的话,一次会抽出 8 条全堆在同一天,那一格就没法看了。
final certain = group.events.where((e) => e.dateCertain).toList();
final pending = group.events.where((e) => !e.dateCertain).toList();
for (final e in certain) {
children.add(_card(e, isDark));
children.add(SizedBox(height: AppKidsDimens.space12));
}
if (pending.isNotEmpty) {
children.add(Padding(
padding: EdgeInsets.only(bottom: AppKidsDimens.space8),
child: Text('memDatePending'.tr,
style: AppKidsTextStyles.caption(isDark)),
));
for (final e in pending) {
children.add(_card(e, isDark));
children.add(SizedBox(height: AppKidsDimens.space12));
}
}
if (showDayHeaders) children.add(SizedBox(height: AppKidsDimens.space8));
}
@ -195,10 +310,47 @@ class TodoScopeList extends GetView<CalendarController> {
);
}
/// 按分类分流。花销一种样式,其余(待办/闹钟/灵感/未知)共用一种。
///
/// ⚠️ 用 if 而不是穷举 switch:分类会随服务端增加,穷举每加一类都得回来改,
/// 漏改就是编译错——而这里只关心「是不是花销」。
Widget _card(CalendarEvent event, bool isDark) {
return event.category == CalendarEventCategory.expense
? _expenseCard(event, isDark)
: _todoCard(event, isDark);
}
/// 打开编辑弹层。
///
/// 先按 memoryId 回查真实的 MemoryItem——日历事件是投影出来的(重复项会摊成
/// 多天,id 带了日期后缀),不能拿它直接当记忆项用。
Future<void> _openEdit(CalendarEvent event) async {
final item = MemoryService.to.items
.firstWhereOrNull((e) => e.id == event.memoryId);
if (item == null) return;
await MemoryEditSheet.show(item: item);
}
Future<void> _toggleDone(CalendarEvent event) async {
final item = MemoryService.to.items
.firstWhereOrNull((e) => e.id == event.memoryId);
if (item == null) return;
final ok = await MemoryService.to.complete([item.id], !item.isDone);
if (!ok) {
Get.snackbar('error'.tr, 'memCompleteFailed'.tr,
snackPosition: SnackPosition.TOP);
}
}
Widget _todoCard(CalendarEvent event, bool isDark) {
final bool done = event.taskStatus == 2;
final Color accent = done ? AppPalette.brand400 : event.color;
return KidsCard(
// 灵感是「记下来」的,没有完成不完成一说,不给勾选框
final bool checkable = event.category != CalendarEventCategory.idea;
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => _openEdit(event),
child: KidsCard(
padding: EdgeInsets.symmetric(
horizontal: AppKidsDimens.space16,
vertical: AppKidsDimens.space12,
@ -215,6 +367,21 @@ class TodoScopeList extends GetView<CalendarController> {
),
),
SizedBox(width: AppKidsDimens.space12),
if (checkable) ...[
GestureDetector(
// 勾选框自己吃掉点击,不要冒泡到卡片的「打开编辑」
behavior: HitTestBehavior.opaque,
onTap: () => _toggleDone(event),
child: Padding(
padding: EdgeInsets.only(right: AppKidsDimens.space8, top: 2.h),
child: Icon(
done ? Icons.check_circle : Icons.radio_button_unchecked,
size: 20.sp,
color: done ? AppPalette.brand400 : accent,
),
),
),
],
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@ -239,16 +406,30 @@ class TodoScopeList extends GetView<CalendarController> {
overflow: TextOverflow.ellipsis,
style: AppKidsTextStyles.caption(isDark),
),
if (event.dueRaw.isNotEmpty) ...[
SizedBox(height: 2.h),
Text(
'memDueRaw'.trParams({'raw': event.dueRaw}),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: AppKidsTextStyles.caption(isDark)
.copyWith(color: event.color),
),
],
],
),
),
],
),
),
);
}
Widget _expenseCard(CalendarEvent event, bool isDark) {
return KidsCard(
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => _openEdit(event),
child: KidsCard(
padding: EdgeInsets.symmetric(
horizontal: AppKidsDimens.space16,
vertical: AppKidsDimens.space12,
@ -288,7 +469,7 @@ class TodoScopeList extends GetView<CalendarController> {
),
SizedBox(width: AppKidsDimens.space8),
Text(
'¥${(event.amount ?? 0).toStringAsFixed(0)}',
'¥${(event.amount ?? 0).toStringAsFixed(2)}',
style: AppKidsTextStyles.body(isDark).copyWith(
fontWeight: FontWeight.w800,
color: event.color,
@ -296,24 +477,42 @@ class TodoScopeList extends GetView<CalendarController> {
),
],
),
),
);
}
/// 空态。
///
/// mock 演示数据删掉之后,新用户看到的第一屏就是这里——所以它必须告诉用户
/// 「怎么加第一条」,而不是只说一句「暂无记录」。
/// 外面套 ListView 是为了让 RefreshIndicator 在空态下也能下拉。
Widget _buildEmpty(bool isDark) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.event_available,
size: 48.sp, color: Colors.grey.withOpacity(0.4)),
SizedBox(height: 10.h),
Text(
'${controller.scopeLabel} ${'calNoRecords'.tr}',
style: AppKidsTextStyles.bodySmall(isDark)
return ListView(
physics: const AlwaysScrollableScrollPhysics(),
padding: EdgeInsets.symmetric(vertical: 60.h),
children: [
Icon(Icons.event_available,
size: 48.sp, color: Colors.grey.withValues(alpha: 0.4)),
SizedBox(height: 12.h),
Center(
child: Text(
'memEmptyTitle'.tr,
style: AppKidsTextStyles.body(isDark)
.copyWith(color: AppKidsColors.textSub(isDark)),
),
],
),
),
SizedBox(height: 6.h),
Center(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 40.w),
child: Text(
'memEmptySubtitle'.tr,
textAlign: TextAlign.center,
style: AppKidsTextStyles.caption(isDark),
),
),
),
],
);
}
}

14
apps/client/pubspec.lock

@ -795,6 +795,14 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
flutter_timezone:
dependency: "direct main"
description:
name: flutter_timezone
sha256: ea53c61c9152f271a5e30624a624184804947b6a733ff2b64186bb2579446892
url: "https://pub.dev"
source: hosted
version: "3.0.1"
flutter_tts:
dependency: "direct main"
description:
@ -1869,13 +1877,13 @@ packages:
source: hosted
version: "0.7.10"
timezone:
dependency: transitive
dependency: "direct main"
description:
name: timezone
sha256: dd14a3b83cfd7cb19e7888f1cbc20f258b8d71b54c06f79ac585f14093a287d1
sha256: "2236ec079a174ce07434e89fcd3fcda430025eb7692244139a9cf54fdcf1fc7d"
url: "https://pub.dev"
source: hosted
version: "0.10.1"
version: "0.9.4"
tobias:
dependency: "direct main"
description:

5
apps/client/pubspec.yaml

@ -59,6 +59,11 @@ dependencies:
path: plugins/window_size
ref: 6c66ad23ee79749f30a8eece542cf54eaf157ed8
flutter_local_notifications: ^18.0.1
# zonedSchedule 排定时提醒必需:flutter_local_notifications 只接受 tz.TZDateTime,
# 没有这两个依赖,拾忆的「提前 N 分钟提醒」一行都写不了。
# timezone 提供时区数据库,flutter_timezone 用来问系统「本机在哪个时区」。
timezone: ^0.9.4
flutter_timezone: ^3.0.1
device_info_plus: ^11.5.0
uuid: ^4.3.3
record: ^6.0.0

42
apps/client/tools/i18n_audit.py

@ -187,6 +187,17 @@ def main() -> int:
default="englishy",
help="exact: value equals baseline; englishy: value equals baseline and looks English",
)
parser.add_argument(
"--fail-on-missing",
action="store_true",
help="缺失 key 数超过 --allow-missing 时以非 0 退出(给 CI 用)",
)
parser.add_argument(
"--allow-missing",
type=int,
default=0,
help="允许的缺失 key 总数基线;配合 --fail-on-missing 卡住「只增不减」",
)
args = parser.parse_args()
lang_dir = args.dir
@ -208,6 +219,17 @@ def main() -> int:
result: dict[str, dict] = {"baseline": args.baseline, "mode": args.mode, "files": {}}
global_counter: Counter[str] = Counter()
# 缺失 key 检查。
#
# ⚠️ 这一段是 2026-09-05 补的,此前本工具**结构上无法发现缺失的 key**:
# 下面那个循环遍历的是「该语言文件已有的 key」,且对 baseline 里没有的直接
# continue —— 一个 key 整个丢掉,它一声不吭。真实后果:tabTodo 在 30 个语言
# 文件里根本不存在,GetX 静默回退英文,谁也没发现,直到有人逐个文件去数。
# 以 zh_cn 为基准(它是文案的源语言,key 最全),不是以 en_us。
ref_name = "zh_cn.dart" if "zh_cn.dart" in parsed else args.baseline
ref_keys = set(parsed[ref_name])
missing_counter: Counter[str] = Counter()
for fn, kv in parsed.items():
if fn == args.baseline:
continue
@ -224,17 +246,35 @@ def main() -> int:
continue
issues.append({"key": k, "value": v})
global_counter[k] += 1
result["files"][fn] = {"count": len(issues), "items": issues}
missing = sorted(ref_keys - set(kv))
for k in missing:
missing_counter[k] += 1
result["files"][fn] = {
"count": len(issues),
"items": issues,
"missing_count": len(missing),
"missing": missing,
}
total_missing = sum(f["missing_count"] for f in result["files"].values())
result["summary"] = {
"file_count": len(dart_files),
"non_baseline_file_count": len(dart_files) - 1,
"files_with_issues": sum(1 for f in result["files"].values() if f["count"] > 0),
"total_issues": sum(f["count"] for f in result["files"].values()),
"most_common_keys": [{"key": k, "count": c} for k, c in global_counter.most_common(50)],
# 缺失统计
"missing_reference": ref_name,
"files_with_missing": sum(1 for f in result["files"].values() if f["missing_count"] > 0),
"total_missing": total_missing,
"most_missing_keys": [{"key": k, "count": c} for k, c in missing_counter.most_common(50)],
}
print(json.dumps(result, ensure_ascii=False, indent=2))
# --fail-on-missing 给 CI 用:新增 key 只补了中文就提交时,这里非 0 退出。
# 不默认开启——仓库现存约 8000 处历史缺失,一上来就红会让人直接把检查关掉。
if args.fail_on_missing and total_missing > args.allow_missing:
return 1
return 0

Loading…
Cancel
Save