From 66463e53f69f778e5255615e496e3c5f552f9f31 Mon Sep 17 00:00:00 2001 From: Rodger-Wang <1367893453@qq.com> Date: Sat, 5 Sep 2026 00:36:10 +0800 Subject: [PATCH] =?UTF-8?q?=E8=AE=B0=E5=BF=86=E4=B8=AD=E5=BF=83=E5=AE=A2?= =?UTF-8?q?=E6=88=B7=E7=AB=AF=EF=BC=9A=E6=8B=BE=E5=BF=86=E9=A1=B5=E6=8E=A5?= =?UTF-8?q?=E6=9C=8D=E5=8A=A1=E7=AB=AF=20+=20=E5=86=99=E6=93=8D=E4=BD=9C?= =?UTF-8?q?=20UI=20+=20=E6=9C=AC=E5=9C=B0=E6=8F=90=E9=86=92=20+=20?= =?UTF-8?q?=E6=AF=8F=E6=97=A5=E6=92=AD=E6=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../lib/core/bindings/initial_binding.dart | 8 + .../lib/core/translations/language/ar_sa.dart | 55 ++ .../lib/core/translations/language/bg_bg.dart | 55 ++ .../lib/core/translations/language/cs_cz.dart | 55 ++ .../lib/core/translations/language/da_dk.dart | 55 ++ .../lib/core/translations/language/de_de.dart | 100 ++-- .../lib/core/translations/language/el_gr.dart | 55 ++ .../lib/core/translations/language/en_us.dart | 100 ++-- .../lib/core/translations/language/es_es.dart | 100 ++-- .../lib/core/translations/language/et_ee.dart | 55 ++ .../lib/core/translations/language/fa_ir.dart | 55 ++ .../lib/core/translations/language/fi_fi.dart | 55 ++ .../lib/core/translations/language/fr_fr.dart | 100 ++-- .../lib/core/translations/language/hi_in.dart | 55 ++ .../lib/core/translations/language/hr_hr.dart | 55 ++ .../lib/core/translations/language/hu_hu.dart | 55 ++ .../lib/core/translations/language/id_id.dart | 55 ++ .../lib/core/translations/language/is_is.dart | 55 ++ .../lib/core/translations/language/it_it.dart | 55 ++ .../lib/core/translations/language/ja_jp.dart | 100 ++-- .../lib/core/translations/language/ko_kr.dart | 100 ++-- .../lib/core/translations/language/lt_lt.dart | 55 ++ .../lib/core/translations/language/lv_lv.dart | 55 ++ .../lib/core/translations/language/ms_my.dart | 55 ++ .../lib/core/translations/language/nb_no.dart | 55 ++ .../lib/core/translations/language/nl_nl.dart | 55 ++ .../lib/core/translations/language/pl_pl.dart | 55 ++ .../lib/core/translations/language/pt_pt.dart | 100 ++-- .../lib/core/translations/language/ro_ro.dart | 55 ++ .../lib/core/translations/language/ru_ru.dart | 100 ++-- .../lib/core/translations/language/sk_sk.dart | 55 ++ .../lib/core/translations/language/sl_si.dart | 55 ++ .../lib/core/translations/language/sr_rs.dart | 55 ++ .../lib/core/translations/language/sv_se.dart | 55 ++ .../lib/core/translations/language/th_th.dart | 55 ++ .../lib/core/translations/language/tl_ph.dart | 55 ++ .../lib/core/translations/language/tr_tr.dart | 55 ++ .../lib/core/translations/language/uk_ua.dart | 55 ++ .../lib/core/translations/language/vi_vn.dart | 55 ++ .../lib/core/translations/language/zh_cn.dart | 98 ++-- .../lib/core/translations/language/zh_hk.dart | 98 ++-- .../lib/core/translations/language/zh_tw.dart | 98 ++-- .../lib/core/utils/app_lifecycle_manager.dart | 35 ++ apps/client/lib/data/models/memory_item.dart | 396 ++++++++++++++ .../lib/data/services/account_service.dart | 18 + .../services/assistant_directive_service.dart | 73 ++- .../services/memory_reminder_scheduler.dart | 254 +++++++++ .../lib/data/services/memory_service.dart | 428 +++++++++++++++ .../client/lib/data/services/network/api.dart | 103 ++++ .../services/network/auth_interceptor.dart | 7 +- .../home/controllers/calendar_controller.dart | 145 +++-- .../widgets/calendar/calendar_event.dart | 240 +++------ .../modules/main_tab/views/tabs/todo_tab.dart | 65 ++- .../views/widgets/memory_daily_sheet.dart | 271 ++++++++++ .../views/widgets/memory_edit_sheet.dart | 495 ++++++++++++++++++ .../views/widgets/todo_scope_list.dart | 241 ++++++++- apps/client/pubspec.lock | 14 +- apps/client/pubspec.yaml | 5 + apps/client/tools/i18n_audit.py | 42 +- 59 files changed, 4839 insertions(+), 745 deletions(-) create mode 100644 apps/client/lib/data/models/memory_item.dart create mode 100644 apps/client/lib/data/services/memory_reminder_scheduler.dart create mode 100644 apps/client/lib/data/services/memory_service.dart create mode 100644 apps/client/lib/modules/main_tab/views/widgets/memory_daily_sheet.dart create mode 100644 apps/client/lib/modules/main_tab/views/widgets/memory_edit_sheet.dart diff --git a/apps/client/lib/core/bindings/initial_binding.dart b/apps/client/lib/core/bindings/initial_binding.dart index 675dd7fa..6f965f9b 100644 --- a/apps/client/lib/core/bindings/initial_binding.dart +++ b/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(), permanent: true); + // 记忆中心(拾忆)。permanent:拾忆页、每日弹窗、提醒排期都要读同一份数据, + // 且 onInit 里要恢复本地缓存,不能随页面销毁。 + Get.put(MemoryService(), permanent: true); + Get.put(MemoryReminderScheduler(), + permanent: true); + // 全局悬浮 AI 球:main.dart 的 builder 里马上要用,必须提前 put(不能 lazyPut) Get.put(AiOrbController(), permanent: true); diff --git a/apps/client/lib/core/translations/language/ar_sa.dart b/apps/client/lib/core/translations/language/ar_sa.dart index 20e496e0..658e4b1b 100644 --- a/apps/client/lib/core/translations/language/ar_sa.dart +++ b/apps/client/lib/core/translations/language/ar_sa.dart @@ -1,5 +1,60 @@ //阿拉伯-沙特阿拉伯语 const Map 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': 'تأكيد', diff --git a/apps/client/lib/core/translations/language/bg_bg.dart b/apps/client/lib/core/translations/language/bg_bg.dart index f94cf673..b2b82cd9 100644 --- a/apps/client/lib/core/translations/language/bg_bg.dart +++ b/apps/client/lib/core/translations/language/bg_bg.dart @@ -1,5 +1,60 @@ //保加利亚-保加利亚语 const Map 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': 'Потвърди', diff --git a/apps/client/lib/core/translations/language/cs_cz.dart b/apps/client/lib/core/translations/language/cs_cz.dart index 4b0fbc25..1e05335e 100644 --- a/apps/client/lib/core/translations/language/cs_cz.dart +++ b/apps/client/lib/core/translations/language/cs_cz.dart @@ -1,5 +1,60 @@ //捷克-捷克语 const Map 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', diff --git a/apps/client/lib/core/translations/language/da_dk.dart b/apps/client/lib/core/translations/language/da_dk.dart index 823beb12..c1d79c16 100644 --- a/apps/client/lib/core/translations/language/da_dk.dart +++ b/apps/client/lib/core/translations/language/da_dk.dart @@ -1,5 +1,60 @@ //丹麦-丹麦语 const Map 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', diff --git a/apps/client/lib/core/translations/language/de_de.dart b/apps/client/lib/core/translations/language/de_de.dart index ab0c617d..cac570d6 100644 --- a/apps/client/lib/core/translations/language/de_de.dart +++ b/apps/client/lib/core/translations/language/de_de.dart @@ -1,5 +1,59 @@ //德语-德语 const Map 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 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 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', diff --git a/apps/client/lib/core/translations/language/el_gr.dart b/apps/client/lib/core/translations/language/el_gr.dart index aee4af11..1c8b5d5f 100644 --- a/apps/client/lib/core/translations/language/el_gr.dart +++ b/apps/client/lib/core/translations/language/el_gr.dart @@ -1,5 +1,60 @@ //希腊-希腊语 const Map 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': 'Επιβεβαίωση', diff --git a/apps/client/lib/core/translations/language/en_us.dart b/apps/client/lib/core/translations/language/en_us.dart index fd5115d5..00926cda 100644 --- a/apps/client/lib/core/translations/language/en_us.dart +++ b/apps/client/lib/core/translations/language/en_us.dart @@ -1,5 +1,59 @@ //美国-英语 const Map 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 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 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', diff --git a/apps/client/lib/core/translations/language/es_es.dart b/apps/client/lib/core/translations/language/es_es.dart index 0772d1f6..18238a0b 100644 --- a/apps/client/lib/core/translations/language/es_es.dart +++ b/apps/client/lib/core/translations/language/es_es.dart @@ -1,5 +1,59 @@ //西班牙-西班牙语 const Map 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 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 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', diff --git a/apps/client/lib/core/translations/language/et_ee.dart b/apps/client/lib/core/translations/language/et_ee.dart index 0f42e167..8ae656f0 100644 --- a/apps/client/lib/core/translations/language/et_ee.dart +++ b/apps/client/lib/core/translations/language/et_ee.dart @@ -1,5 +1,60 @@ //爱沙尼亚-爱沙尼亚语 const Map 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', diff --git a/apps/client/lib/core/translations/language/fa_ir.dart b/apps/client/lib/core/translations/language/fa_ir.dart index fa0e6b24..f4feb28e 100644 --- a/apps/client/lib/core/translations/language/fa_ir.dart +++ b/apps/client/lib/core/translations/language/fa_ir.dart @@ -1,5 +1,60 @@ //伊朗-波斯语 const Map 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': 'تأیید', diff --git a/apps/client/lib/core/translations/language/fi_fi.dart b/apps/client/lib/core/translations/language/fi_fi.dart index 4fd4b4b9..1b487996 100644 --- a/apps/client/lib/core/translations/language/fi_fi.dart +++ b/apps/client/lib/core/translations/language/fi_fi.dart @@ -1,5 +1,60 @@ //芬兰-芬兰语 const Map 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', diff --git a/apps/client/lib/core/translations/language/fr_fr.dart b/apps/client/lib/core/translations/language/fr_fr.dart index a7e479a2..be40cfea 100644 --- a/apps/client/lib/core/translations/language/fr_fr.dart +++ b/apps/client/lib/core/translations/language/fr_fr.dart @@ -1,5 +1,59 @@ // 法国-法语 const Map 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 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 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', diff --git a/apps/client/lib/core/translations/language/hi_in.dart b/apps/client/lib/core/translations/language/hi_in.dart index 86235074..dde8daea 100644 --- a/apps/client/lib/core/translations/language/hi_in.dart +++ b/apps/client/lib/core/translations/language/hi_in.dart @@ -1,5 +1,60 @@ // 印地-印地语 const Map 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': 'पुष्टि करें', diff --git a/apps/client/lib/core/translations/language/hr_hr.dart b/apps/client/lib/core/translations/language/hr_hr.dart index 8aaa0f43..9c07e28d 100644 --- a/apps/client/lib/core/translations/language/hr_hr.dart +++ b/apps/client/lib/core/translations/language/hr_hr.dart @@ -1,5 +1,60 @@ // 克罗地亚-克罗地亚语 const Map 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', diff --git a/apps/client/lib/core/translations/language/hu_hu.dart b/apps/client/lib/core/translations/language/hu_hu.dart index cc6c873e..a33deaad 100644 --- a/apps/client/lib/core/translations/language/hu_hu.dart +++ b/apps/client/lib/core/translations/language/hu_hu.dart @@ -1,5 +1,60 @@ //匈牙利-匈牙利语 const Map 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', diff --git a/apps/client/lib/core/translations/language/id_id.dart b/apps/client/lib/core/translations/language/id_id.dart index 61c1028d..1545682f 100644 --- a/apps/client/lib/core/translations/language/id_id.dart +++ b/apps/client/lib/core/translations/language/id_id.dart @@ -1,5 +1,60 @@ //印尼-印尼语 const Map 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', diff --git a/apps/client/lib/core/translations/language/is_is.dart b/apps/client/lib/core/translations/language/is_is.dart index 3ca11fc9..2a0d0767 100644 --- a/apps/client/lib/core/translations/language/is_is.dart +++ b/apps/client/lib/core/translations/language/is_is.dart @@ -1,5 +1,60 @@ // 冰岛-冰岛语 const Map 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', diff --git a/apps/client/lib/core/translations/language/it_it.dart b/apps/client/lib/core/translations/language/it_it.dart index c4ed1c16..69d166cf 100644 --- a/apps/client/lib/core/translations/language/it_it.dart +++ b/apps/client/lib/core/translations/language/it_it.dart @@ -1,5 +1,60 @@ // 意大利-意大利语 const Map 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', diff --git a/apps/client/lib/core/translations/language/ja_jp.dart b/apps/client/lib/core/translations/language/ja_jp.dart index a2eb95b8..14754f5e 100644 --- a/apps/client/lib/core/translations/language/ja_jp.dart +++ b/apps/client/lib/core/translations/language/ja_jp.dart @@ -1,5 +1,59 @@ // 日本-日本語 const Map 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 jaJP = { 'meetingRecordScreenCapturePermissionDenied': 'システム音声を録音するには「画面収録」の許可が必要です。許可してから再試行してください', 'tabDevice': 'デバイス', - 'tabTodo': 'Remin', + 'tabTodo': 'ふりかえり', 'tabPlaza': 'プラザ', 'sloganHome': 'テクノロジーでスマートな毎日を', 'sloganAi': '無限の知性、無限の可能性', @@ -1735,50 +1789,6 @@ const Map 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': '月', diff --git a/apps/client/lib/core/translations/language/ko_kr.dart b/apps/client/lib/core/translations/language/ko_kr.dart index 87bbd9ae..80268ea1 100644 --- a/apps/client/lib/core/translations/language/ko_kr.dart +++ b/apps/client/lib/core/translations/language/ko_kr.dart @@ -1,5 +1,59 @@ // 韩国-韩语 const Map 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 koKR = { 'currentConnection': '현재 연결', 'binding': '등록 중…', 'tabDevice': '기기', - 'tabTodo': 'Remin', + 'tabTodo': '되새김', 'tabPlaza': '플라자', 'sloganHome': '기술로 더 스마트한 일상을', 'sloganAi': '무한한 지능, 무한한 가능성', @@ -1741,50 +1795,6 @@ const Map 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': '월', diff --git a/apps/client/lib/core/translations/language/lt_lt.dart b/apps/client/lib/core/translations/language/lt_lt.dart index f5c719e4..4d098168 100644 --- a/apps/client/lib/core/translations/language/lt_lt.dart +++ b/apps/client/lib/core/translations/language/lt_lt.dart @@ -1,4 +1,59 @@ const Map 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', diff --git a/apps/client/lib/core/translations/language/lv_lv.dart b/apps/client/lib/core/translations/language/lv_lv.dart index 01f173fc..6bf59e91 100644 --- a/apps/client/lib/core/translations/language/lv_lv.dart +++ b/apps/client/lib/core/translations/language/lv_lv.dart @@ -1,4 +1,59 @@ const Map 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', diff --git a/apps/client/lib/core/translations/language/ms_my.dart b/apps/client/lib/core/translations/language/ms_my.dart index ccd946ec..95ed50c3 100644 --- a/apps/client/lib/core/translations/language/ms_my.dart +++ b/apps/client/lib/core/translations/language/ms_my.dart @@ -1,5 +1,60 @@ // 马来西亚-马来语 const Map 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', diff --git a/apps/client/lib/core/translations/language/nb_no.dart b/apps/client/lib/core/translations/language/nb_no.dart index c2c10653..12f432f3 100644 --- a/apps/client/lib/core/translations/language/nb_no.dart +++ b/apps/client/lib/core/translations/language/nb_no.dart @@ -1,5 +1,60 @@ // 挪威-挪威语 const Map 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', diff --git a/apps/client/lib/core/translations/language/nl_nl.dart b/apps/client/lib/core/translations/language/nl_nl.dart index e2317bbe..78681dd3 100644 --- a/apps/client/lib/core/translations/language/nl_nl.dart +++ b/apps/client/lib/core/translations/language/nl_nl.dart @@ -1,5 +1,60 @@ // 荷兰-荷兰语 const Map 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', diff --git a/apps/client/lib/core/translations/language/pl_pl.dart b/apps/client/lib/core/translations/language/pl_pl.dart index 6a8db691..8b804573 100644 --- a/apps/client/lib/core/translations/language/pl_pl.dart +++ b/apps/client/lib/core/translations/language/pl_pl.dart @@ -1,5 +1,60 @@ // 波兰-波兰语 const Map 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ź', diff --git a/apps/client/lib/core/translations/language/pt_pt.dart b/apps/client/lib/core/translations/language/pt_pt.dart index e0b269d5..7d5e67a5 100644 --- a/apps/client/lib/core/translations/language/pt_pt.dart +++ b/apps/client/lib/core/translations/language/pt_pt.dart @@ -1,5 +1,59 @@ // 葡萄牙-葡萄牙语 const Map 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 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 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', diff --git a/apps/client/lib/core/translations/language/ro_ro.dart b/apps/client/lib/core/translations/language/ro_ro.dart index 3b9d18af..615527bc 100644 --- a/apps/client/lib/core/translations/language/ro_ro.dart +++ b/apps/client/lib/core/translations/language/ro_ro.dart @@ -1,5 +1,60 @@ // 罗马尼亚-罗马尼亚语 const Map 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ă', diff --git a/apps/client/lib/core/translations/language/ru_ru.dart b/apps/client/lib/core/translations/language/ru_ru.dart index 674c9950..f6d3100e 100644 --- a/apps/client/lib/core/translations/language/ru_ru.dart +++ b/apps/client/lib/core/translations/language/ru_ru.dart @@ -1,5 +1,59 @@ // 俄罗斯-俄语 const Map 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 ruRU = { 'binding': 'Привязка…', 'microphonePermission': 'Микрофон', 'tabDevice': 'Устройства', - 'tabTodo': 'Remin', + 'tabTodo': 'Воспоминания', 'tabPlaza': 'Плаза', 'sloganHome': 'Технологии для умной жизни', 'sloganAi': 'Безграничный интеллект', @@ -1924,50 +1978,6 @@ const Map 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': 'Месяц', diff --git a/apps/client/lib/core/translations/language/sk_sk.dart b/apps/client/lib/core/translations/language/sk_sk.dart index 5897bf70..037ec1b8 100644 --- a/apps/client/lib/core/translations/language/sk_sk.dart +++ b/apps/client/lib/core/translations/language/sk_sk.dart @@ -1,5 +1,60 @@ // 斯洛伐克-斯洛伐克语 const Map 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ť', diff --git a/apps/client/lib/core/translations/language/sl_si.dart b/apps/client/lib/core/translations/language/sl_si.dart index 44c2c03b..7b25fc1b 100644 --- a/apps/client/lib/core/translations/language/sl_si.dart +++ b/apps/client/lib/core/translations/language/sl_si.dart @@ -1,5 +1,60 @@ // 斯洛文尼亚-斯洛文尼亚语 const Map 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', diff --git a/apps/client/lib/core/translations/language/sr_rs.dart b/apps/client/lib/core/translations/language/sr_rs.dart index 5dacca4c..7868b136 100644 --- a/apps/client/lib/core/translations/language/sr_rs.dart +++ b/apps/client/lib/core/translations/language/sr_rs.dart @@ -1,5 +1,60 @@ // 塞尔维亚-塞尔维亚语 const Map 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', diff --git a/apps/client/lib/core/translations/language/sv_se.dart b/apps/client/lib/core/translations/language/sv_se.dart index 886b3906..7992fcdf 100644 --- a/apps/client/lib/core/translations/language/sv_se.dart +++ b/apps/client/lib/core/translations/language/sv_se.dart @@ -1,5 +1,60 @@ //瑞典-瑞典语 const Map 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', diff --git a/apps/client/lib/core/translations/language/th_th.dart b/apps/client/lib/core/translations/language/th_th.dart index 1020a9c1..36b1bbc9 100644 --- a/apps/client/lib/core/translations/language/th_th.dart +++ b/apps/client/lib/core/translations/language/th_th.dart @@ -1,5 +1,60 @@ //泰国-泰语 const Map 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': 'ยืนยัน', diff --git a/apps/client/lib/core/translations/language/tl_ph.dart b/apps/client/lib/core/translations/language/tl_ph.dart index fb3b3fe2..4128b842 100644 --- a/apps/client/lib/core/translations/language/tl_ph.dart +++ b/apps/client/lib/core/translations/language/tl_ph.dart @@ -1,5 +1,60 @@ //菲律宾-塔加路语 const Map 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', diff --git a/apps/client/lib/core/translations/language/tr_tr.dart b/apps/client/lib/core/translations/language/tr_tr.dart index 6c22252d..cedc6c1c 100644 --- a/apps/client/lib/core/translations/language/tr_tr.dart +++ b/apps/client/lib/core/translations/language/tr_tr.dart @@ -1,5 +1,60 @@ //土耳其-土耳其语 const Map 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', diff --git a/apps/client/lib/core/translations/language/uk_ua.dart b/apps/client/lib/core/translations/language/uk_ua.dart index 72549837..fe01f752 100644 --- a/apps/client/lib/core/translations/language/uk_ua.dart +++ b/apps/client/lib/core/translations/language/uk_ua.dart @@ -1,5 +1,60 @@ // 乌克兰-乌克兰语 const Map 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': 'Підтвердити', diff --git a/apps/client/lib/core/translations/language/vi_vn.dart b/apps/client/lib/core/translations/language/vi_vn.dart index 73a93911..669dc159 100644 --- a/apps/client/lib/core/translations/language/vi_vn.dart +++ b/apps/client/lib/core/translations/language/vi_vn.dart @@ -1,5 +1,60 @@ //越南-越南语 const Map 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', diff --git a/apps/client/lib/core/translations/language/zh_cn.dart b/apps/client/lib/core/translations/language/zh_cn.dart index dff0e641..189066c0 100644 --- a/apps/client/lib/core/translations/language/zh_cn.dart +++ b/apps/client/lib/core/translations/language/zh_cn.dart @@ -1,5 +1,59 @@ //中国-简体中文 const Map 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 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': '月', diff --git a/apps/client/lib/core/translations/language/zh_hk.dart b/apps/client/lib/core/translations/language/zh_hk.dart index 7bdd4b32..65713b03 100644 --- a/apps/client/lib/core/translations/language/zh_hk.dart +++ b/apps/client/lib/core/translations/language/zh_hk.dart @@ -1,5 +1,59 @@ //香港-繁体中文 const Map 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 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': '月', diff --git a/apps/client/lib/core/translations/language/zh_tw.dart b/apps/client/lib/core/translations/language/zh_tw.dart index 3cfc825b..803561d4 100644 --- a/apps/client/lib/core/translations/language/zh_tw.dart +++ b/apps/client/lib/core/translations/language/zh_tw.dart @@ -1,5 +1,59 @@ //台湾-繁体中文 const Map 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 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': '月', diff --git a/apps/client/lib/core/utils/app_lifecycle_manager.dart b/apps/client/lib/core/utils/app_lifecycle_manager.dart index 8685e0ff..4d97f4d9 100644 --- a/apps/client/lib/core/utils/app_lifecycle_manager.dart +++ b/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 _refreshMemoryOnResume() async { + if (!Get.isRegistered()) return; + // 未登录时拿不到数据,直接跳过(登录后由登录流程自己触发一次) + if (!User.isLoggedIn()) return; + try { + // 先拉当天数据顺便更新缓存,再决定弹不弹 + await MemoryDailySheet.maybeShow(); + } catch (e) { + Logger.warning('拾忆每日弹窗失败(已忽略): $e'); + } + try { + if (Get.isRegistered()) { + await MemoryReminderScheduler.to.reschedule(); + } + } catch (e) { + Logger.warning('拾忆提醒重排失败(已忽略): $e'); + } + } + /// 处理应用进入后台 void _handleAppPaused(AppLifecycleState state) { // Logger.info('应用进入后台: $state'); diff --git a/apps/client/lib/data/models/memory_item.dart b/apps/client/lib/data/models/memory_item.dart new file mode 100644 index 00000000..6d232331 --- /dev/null +++ b/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 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 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 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 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 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 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 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(); diff --git a/apps/client/lib/data/services/account_service.dart b/apps/client/lib/data/services/account_service.dart index 08b63c66..fd26e746 100644 --- a/apps/client/lib/data/services/account_service.dart +++ b/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.to.clear(); + } + if (Get.isRegistered()) { + 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.to.clear(); + } + if (Get.isRegistered()) { + unawaited(MemoryReminderScheduler.to.clear()); + } logger.d('所有用户数据清理成功'); return true; } catch (e) { diff --git a/apps/client/lib/data/services/assistant_directive_service.dart b/apps/client/lib/data/services/assistant_directive_service.dart index 9a85fcc4..6ce18b6b 100644 --- a/apps/client/lib/data/services/assistant_directive_service.dart +++ b/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 _uploadReminder(AssistantReminder r) async { + if (!Get.isRegistered()) 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; + } } /// 用户在代办页勾选完成 diff --git a/apps/client/lib/data/services/memory_reminder_scheduler.dart b/apps/client/lib/data/services/memory_reminder_scheduler.dart new file mode 100644 index 00000000..da90e39c --- /dev/null +++ b/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(); + 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 _scheduledIds = []; + + /// 权限被拒时置 true,页面据此显示一条静态提示(不是弹窗)指向权限管理页 + final RxBool permissionDenied = false.obs; + + Future _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 _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; + 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 _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 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 _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 clear() async { + await _ensurePlugin(); + await _cancelAllOurs(); + } +} diff --git a/apps/client/lib/data/services/memory_service.dart b/apps/client/lib/data/services/memory_service.dart new file mode 100644 index 00000000..7eb7fd29 --- /dev/null +++ b/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(); + 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 items = [].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(_kItems); + if (raw == null) return; + final list = raw + .map((e) => MemoryItem.fromJson(Map.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? _data(dynamic resp) { + if (resp == null) return null; + if (resp.data == null) return null; + if (resp.data is Map) return Map.from(resp.data as Map); + return null; + } + + List _parseItems(dynamic raw) { + if (raw is! List) return []; + final out = []; + for (final e in raw) { + try { + out.add(MemoryItem.fromJson(Map.from(e as Map))); + } catch (err) { + // 单条解析失败只跳过这一条。一条脏数据不该让整页空白—— + // appconfig_model 那边就是因为 non-nullable 字段让整个 fromJson 挂掉, + // 表现成设备列表整体不显示。 + Logger.w(_tag, '跳过一条无法解析的记忆项: $err'); + } + } + return out; + } + + // ===================== 读 ===================== + + /// 拉取日期范围内的记忆项,结果**合并**进 [items](不是整体替换)。 + /// + /// 合并而不是替换:日历会按需拉不同月份,整体替换会让上个月的数据在切回去时消失。 + Future load({ + DateTime? start, + DateTime? end, + List? 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 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> 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 _localToday() { + final today = _fmtDate(DateTime.now()); + return items.where((e) => e.happenDate == today).toList(); + } + + /// 未来 N 天的提醒时刻(重复项已由服务端展开成绝对时刻) + Future> 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.from(e as Map))) + .toList(); + } catch (e) { + Logger.w(_tag, 'loadUpcoming 异常: $e'); + return []; + } + } + + // ===================== 写 ===================== + + /// 新增。成功返回落库后的记录(含服务端分配的 id),失败返回 null。 + Future 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.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 update(MemoryItem item, {List? 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.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 remove(List 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 complete(List ids, bool done) async { + if (ids.isEmpty) return true; + final before = {}; + 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 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.from(r as Map)); + } catch (e) { + Logger.w(_tag, 'loadPendingReport 异常: $e'); + return null; + } + } + + Future 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 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 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 expenseCentsByCurrency(DateTime from, DateTime to) { + final out = {}; + 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 parseStat(String statJson) { + if (statJson.isEmpty) return {}; + try { + final v = jsonDecode(statJson); + return v is Map ? Map.from(v) : {}; + } catch (_) { + return {}; + } + } +} diff --git a/apps/client/lib/data/services/network/api.dart b/apps/client/lib/data/services/network/api.dart index 58555015..419fdfe4 100644 --- a/apps/client/lib/data/services/network/api.dart +++ b/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( diff --git a/apps/client/lib/data/services/network/auth_interceptor.dart b/apps/client/lib/data/services/network/auth_interceptor.dart index c4907a42..2b596c46 100644 --- a/apps/client/lib/data/services/network/auth_interceptor.dart +++ b/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 字段就是英文内部状态词 diff --git a/apps/client/lib/modules/home/controllers/calendar_controller.dart b/apps/client/lib/modules/home/controllers/calendar_controller.dart index 556ab666..86ce2423 100644 --- a/apps/client/lib/modules/home/controllers/calendar_controller.dart +++ b/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 events = [].obs; /// 监听助手提醒变化的 worker,onClose 要停掉 - Worker? _reminderWorker; + Worker? _memoryWorker; // 折叠态横向条的代办/花销筛选:null=全部,非空=只看该类别。 // 点击"代办"/"花销"标签设置;点击日期(onDaySelected)重置为全部。 final Rx categoryFilter = Rx(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()) { - _reminderWorker = ever>( - AssistantDirectiveService.to.reminders, + // 服务端数据变了(拉取完成、用户增删改、EMAI 落了新闹钟)就立刻重投影, + // 不用退出重进页面。 + if (Get.isRegistered()) { + _memoryWorker = ever>( + MemoryService.to.items, (_) => _rebuildEvents(), ); } } - /// mock 演示数据 + EMAI 助手落下来的提醒 + /// 从 [MemoryService] 单一来源投影成日历事件。 + /// + /// 重复项在服务端是**一行记录**(repeat_rule=daily/weekly…),日历要按天展示, + /// 所以在可见窗口内逐日摊开——这段展开逻辑是从原来的助手提醒那套留用的, + /// 只是输入从 AssistantReminder 换成了 MemoryItem。 void _rebuildEvents() { - final list = [...mockCalendarEvents()]; - if (Get.isRegistered()) { - for (final r in AssistantDirectiveService.to.reminders) { - list.addAll(_expandReminder(r)); - } + if (!Get.isRegistered()) { + events.clear(); + return; + } + final list = []; + for (final item in MemoryService.to.items) { + list.addAll(_expandItem(item)); } events.assignAll(list); } - /// 把一条提醒摊成日历事件。 + /// 把一条记忆项摊成日历事件。 /// - /// 重复提醒在协议里只有一条记录(`repeat=每天` / `周一`),日历要按天展示, - /// 所以在可见窗口内逐日摊开。窗口取「今天前 7 天 ~ 后 60 天」:往前留几天是 - /// 为了让用户还能看到刚过去的提醒,往后 60 天覆盖月视图翻两页。 - List _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 _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 = []; 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 refreshFromServer({bool silent = false}) async { + if (!Get.isRegistered()) 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 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(); } /// 选中日期的花销合计(元) diff --git a/apps/client/lib/modules/home/views/widgets/calendar/calendar_event.dart b/apps/client/lib/modules/home/views/widgets/calendar/calendar_event.dart index efe71126..8efab545 100644 --- a/apps/client/lib/modules/home/views/widgets/calendar/calendar_event.dart +++ b/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 mockCalendarEvents() { - final now = DateTime.now(); - DateTime d(int offsetDays) => - DateTime(now.year, now.month, now.day).add(Duration(days: offsetDays)); - - return [ - // 今天 - 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, - ), - ]; -} diff --git a/apps/client/lib/modules/main_tab/views/tabs/todo_tab.dart b/apps/client/lib/modules/main_tab/views/tabs/todo_tab.dart index 8a57806f..779751c5 100644 --- a/apps/client/lib/modules/main_tab/views/tabs/todo_tab.dart +++ b/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 createState() => _TodoTabState(); +} + +class _TodoTabState extends State { + @override + void initState() { + super.initState(); + // 首次进入拉一次。不 await——页面要立刻可见,数据到了 Obx 自己会刷。 + WidgetsBinding.instance.addPostFrameCallback((_) { + if (Get.isRegistered()) { + Get.find().refreshFromServer(silent: true); + } + }); + } + + Future _addNew() async { + final ctl = Get.isRegistered() + ? Get.find() + : 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()) { - Get.put(CalendarController()); + ); } - return Get.find(); } diff --git a/apps/client/lib/modules/main_tab/views/widgets/memory_daily_sheet.dart b/apps/client/lib/modules/main_tab/views/widgets/memory_daily_sheet.dart new file mode 100644 index 00000000..c5a55ad1 --- /dev/null +++ b/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(_kLastDate) == _today(); + } catch (_) { + return false; + } + } + + /// 尝试弹出。已经弹过、没有数据、或正在弹时静默跳过。 + /// + /// 调用方**不要 await**它的网络部分——见类注释。 + static Future 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 _show(List items) async { + await Get.bottomSheet( + _DailySheetView(items: items), + isScrollControlled: true, + isDismissible: true, + ); + } + + /// 拼播报文案。**本地拼,不调 LLM**——这段话的结构是固定的, + /// 花一次模型调用换一句同样的话不值得,而且启动阶段等不起。 + static String buildSpeech(List 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 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 _speak() async { + // ⚠️ 通话翻译进行中一律不播报。两者共用同一个 AVAudioSession, + // 在对方刚调好的会话上再设一次会把通话音频路由弄乱(iOS 那条 phone_call + // 分支被改坏过三次)。宁可这次不播,也不能干扰正在进行的通话。 + if (_isTranslationBusy()) { + Logger.i(MemoryDailySheet._tag, '通话翻译进行中,跳过播报'); + return; + } + if (!Get.isRegistered()) return; + try { + _tts = Get.find(); + 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 _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), + ), + ), + ], + ), + ); + } +} diff --git a/apps/client/lib/modules/main_tab/views/widgets/memory_edit_sheet.dart b/apps/client/lib/modules/main_tab/views/widgets/memory_edit_sheet.dart new file mode 100644 index 00000000..16152aac --- /dev/null +++ b/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 show({ + MemoryItem? item, + DateTime? defaultDate, + String defaultCategory = MemoryCategory.todo, + }) async { + final r = await Get.bottomSheet( + MemoryEditSheet( + item: item, + defaultDate: defaultDate, + defaultCategory: defaultCategory, + ), + isScrollControlled: true, + ignoreSafeArea: false, + ); + return r ?? false; + } + + @override + State createState() => _MemoryEditSheetState(); +} + +class _MemoryEditSheetState extends State { + 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? 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( + 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( + 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 _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 _delete() async { + final confirmed = await Get.dialog( + 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); + } + } +} diff --git a/apps/client/lib/modules/main_tab/views/widgets/todo_scope_list.dart b/apps/client/lib/modules/main_tab/views/widgets/todo_scope_list.dart index 1ac45bae..e957f276 100644 --- a/apps/client/lib/modules/main_tab/views/widgets/todo_scope_list.dart +++ b/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 { 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 { 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 { ); } + /// 按分类分流。花销一种样式,其余(待办/闹钟/灵感/未知)共用一种。 + /// + /// ⚠️ 用 if 而不是穷举 switch:分类会随服务端增加,穷举每加一类都得回来改, + /// 漏改就是编译错——而这里只关心「是不是花销」。 + Widget _card(CalendarEvent event, bool isDark) { + return event.category == CalendarEventCategory.expense + ? _expenseCard(event, isDark) + : _todoCard(event, isDark); + } + + /// 打开编辑弹层。 + /// + /// 先按 memoryId 回查真实的 MemoryItem——日历事件是投影出来的(重复项会摊成 + /// 多天,id 带了日期后缀),不能拿它直接当记忆项用。 + Future _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 _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 { ), ), 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 { 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 { ), 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 { ), ], ), + ), ); } + /// 空态。 + /// + /// 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), + ), + ), + ), + ], ); } } diff --git a/apps/client/pubspec.lock b/apps/client/pubspec.lock index 0b195f50..cbc2c8a1 100644 --- a/apps/client/pubspec.lock +++ b/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: diff --git a/apps/client/pubspec.yaml b/apps/client/pubspec.yaml index 2845edd6..bb81320e 100644 --- a/apps/client/pubspec.yaml +++ b/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 diff --git a/apps/client/tools/i18n_audit.py b/apps/client/tools/i18n_audit.py index c308c203..d3e7e963 100644 --- a/apps/client/tools/i18n_audit.py +++ b/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