# Habo 实现规格文档 > 基于 REQUIREMENTS.md 和 ARCHITECTURE.md,定义每个功能的具体实现逻辑、算法和交互流程 --- ## 1. 连续天数 (Streak) 算法 ### 1.1 普通模式 (`_updateLastStreakNormal`) ``` 输入: habitData.events (SplayTreeMap, 按日期升序排列) 输出: streak 值, streakVisible (bool), orangeStreak (bool) 算法: 1. 从最后一天开始,向前遍历 events 2. 初始化 inStreak = 0 3. 对每个事件(从后往前): a. 如果 DayType == clear → 跳过 b. 如果日期间隔 > 1 天 → 断开,结束循环 c. 如果 DayType == check → inStreak++ d. 如果 DayType == progress (数值型): - 如果 progressValue >= targetValue → inStreak++ - 否则 → 跳过 e. 如果 DayType == fail 或 skip → 断开,结束循环 4. streak = inStreak 5. streakVisible = (streak >= 2) 6. orangeStreak = false ``` ### 1.2 两天法则模式 (`_updateLastStreakTwoDay`) ``` 输入: habitData.events, habitData.twoDayRule == true 输出: streak 值, streakVisible, orangeStreak 算法: 1. 从最后一天开始,向前遍历 events 2. 变量: inStreak = 0, usingTwoDayRule = false 3. 对每个事件(从后往前): a. 如果 DayType == clear → 跳过 b. 如果日期间隔 > 1 天 → 断开,结束循环 c. 如果 DayType == check → inStreak++, usingTwoDayRule = false d. 如果 DayType == progress (数值型) 且 progress >= target → inStreak++, usingTwoDayRule = false e. 如果 DayType == fail: - 如果 usingTwoDayRule == true → 断开,结束循环(连续两天失败) - 如果 usingTwoDayRule == false → usingTwoDayRule = true, 不增加 inStreak f. 如果 DayType == skip: - 如果 usingTwoDayRule == true → 断开 - 否则 → 跳过(不影响连续) 4. streak = inStreak 5. streakVisible = (streak >= 2) 6. orangeStreak = usingTwoDayRule ← 橙色表示"处于危险中" ``` **两天法则图解**: ``` 情况 1: ✅ ✅ ❌ ✅ ✅ → streak = 5 (✅ 失败一天后立即恢复) 情况 2: ✅ ✅ ❌ ❌ ✅ → streak = 0 (✅ 连续两天失败,归零) 情况 3: ✅ ✅ ❌ ⏭ ✅ → streak = 0 (⏭ 在两天法则期间跳过,归零) 情况 4: ✅ ✅ ⏭ ✅ ✅ → streak = 5 (⏭ 跳过不影响连续) ``` --- ## 2. 日历事件交互流程 ### 2.1 日历日期点击 ``` 用户点击日历日期 │ ├── 检查 oneTapCheck 设置 │ │ │ ├── oneTapCheck == true (一键打卡模式): │ │ │ │ │ ├── 布尔型习惯: │ │ │ ├── 当前无事件 → 创建 [DayType.check, ""] │ │ │ └── 当前有事件 → 删除事件 (设为 clear) │ │ │ │ │ └── 数值型习惯: │ │ ├── 当前无事件 → 创建 [DayType.check, "", targetValue, targetValue] │ │ └── 当前有事件 → 删除事件 │ │ │ └── oneTapCheck == false (菜单模式): │ │ │ └── 弹出选择菜单,6 个选项: │ │ │ ├── 📅 Date → 修改日期选择器 │ │ │ ├── ✅ Check → 标记完成 │ │ ├── 布尔型: 事件 = [DayType.check, ""] │ │ └── 数值型: 事件 = [DayType.check, "", targetValue, targetValue] │ │ └── 播放 check 音效 │ │ └── 如果 showReward → 显示奖励通知 │ │ │ ├── ➕ Plus/Progress → 数值型专用 │ │ └── 弹出 ProgressInputModal │ │ ├── 圆形进度指示器 (120px) │ │ ├── 当前值 / 目标值 显示 │ │ ├── 快捷按钮: +partialValue, -partialValue │ │ ├── 直接输入文本框 │ │ └── "Complete" 按钮直接设为目标值 │ │ │ ├── ❌ Fail → 标记失败 │ │ └── 事件 = [DayType.fail, ""] │ │ └── 播放 click 音效 │ │ └── 如果 showSanction → 显示惩罚通知 │ │ │ ├── ⏭ Skip → 标记跳过 │ │ └── 事件 = [DayType.skip, ""] │ │ └── 播放 click 音效 │ │ │ └── 💬 Note → 添加备注 │ └── 弹出文本输入对话框 │ └── 保留原有事件类型,更新 comment │ ▼ HabitsManager.addEvent(habitId, date, eventData) │ ├── 更新内存: habitData.events[date] = eventData ├── 写入数据库: EventRepository → SQLite REPLACE ├── 更新连续天数: _updateLastStreak() ├── 更新桌面小组件: HomeWidgetService.update() └── notifyListeners() → UI 重建 ``` ### 2.2 事件数据结构 ```dart // 布尔型习惯 [DayType.check, ""] // 完成 [DayType.fail, ""] // 失败 [DayType.skip, ""] // 跳过 [DayType.check, "好的开始"] // 完成 + 备注 // 数值型习惯 [DayType.check, "", 5.0, 5.0] // 完成 (5/5 km) [DayType.progress, "", 3.5, 5.0] // 部分进度 (3.5/5 km) [DayType.fail, ""] // 失败 // 数组索引: // [0] = DayType (枚举) // [1] = comment (String) // [2] = progressValue (double, 数值型) // [3] = targetValue (double, 数值型) ``` --- ## 3. 统计计算算法 ### 3.1 数据结构 ```dart class StatisticsData { String title; // 习惯标题 int topStreak = 0; // 最高连续天数 int actualStreak = 0; // 当前连续(遍历中) int checks = 0; // 完成次数 int fails = 0; // 失败次数 int skips = 0; // 跳过次数 int progress = 0; // 进度次数 SplayTreeMap>> monthlyTracking; // key = year * 100 + month (如 202604) // value = { DayType: [day1, day2, ...] } } class OverallStatisticsData { int totalChecks; int totalFails; int totalSkips; int totalProgress; } ``` ### 3.2 计算流程 ``` Statistics.calculateStatistics(habits): │ ├── 1. 创建 AllStatistics 容器 │ ├── 2. 遍历每个 habit: │ │ │ ├── 创建 StatisticsData │ │ │ ├── 3. 遍历 events (按日期升序): │ │ │ │ │ ├── 计算日期间隔: │ │ │ └── 如果间隔 > 1 天 → actualStreak 归零 │ │ │ │ │ ├── DayType.check: │ │ │ ├── checks++ │ │ │ ├── actualStreak++ │ │ │ └── if actualStreak > topStreak → topStreak = actualStreak │ │ │ │ │ ├── DayType.progress: │ │ │ ├── progress++ │ │ │ └── if 数值型 && progressValue >= targetValue: │ │ │ ├── actualStreak++ │ │ │ └── update topStreak │ │ │ │ │ ├── DayType.fail: │ │ │ ├── fails++ │ │ │ └── if twoDayRule: │ │ │ ├── if usingTwoDayRule → actualStreak = 0 │ │ │ └── else → usingTwoDayRule = true │ │ │ └── else → actualStreak = 0 │ │ │ │ │ ├── DayType.skip: │ │ │ ├── skips++ │ │ │ └── if usingTwoDayRule → actualStreak = 0 │ │ │ │ │ └── 记录到 monthlyTracking: │ │ └── key = year * 100 + month │ │ └── monthlyTracking[key][dayType].add(day) │ │ │ └── 添加到 allStatistics │ └── 4. 汇总 OverallStatisticsData: ├── totalChecks = sum(各习惯 checks) ├── totalFails = sum(各习惯 fails) ├── totalSkips = sum(各习惯 skips) └── totalProgress = sum(各习惯 progress) ``` --- ## 4. 习惯 CRUD 交互流程 ### 4.1 创建习惯 ``` 用户点击 FAB (+) │ └── AppStateManager.goCreateHabit(true) │ └── EditHabitScreen(isNew: true) │ ├── 表单字段: │ ├── title (必填, 不能为空) │ ├── habitType (下拉: Checkable / Progressive) │ │ └── 如果 Progressive: │ │ ├── targetValue (NumberFormat('#.##')) │ │ ├── partialValue │ │ └── unit │ ├── twoDayRule (Checkbox) │ ├── categories (多选) │ ├── notification (Checkbox) │ │ └── notTime (TimePicker) │ └── [展开] Advanced: │ ├── cue (提示触发器) │ ├── routine (例行动作) │ ├── reward (奖励) │ ├── showReward (显示奖励通知) │ ├── sanction (惩罚) │ ├── showSanction (显示惩罚通知) │ └── accountant (问责伙伴) │ └── 点击保存 (FAB ✓): ├── 验证 title 非空 ├── 创建 HabitData: │ ├── id: null (数据库自增) │ ├── position: activeHabits.length (追加到末尾) │ └── ... 各字段 ├── HabitsManager.addHabit(habit) │ ├── HabitRepository.createHabit() → INSERT │ ├── CategoryRepository.updateHabitCategories() │ ├── 如果 notification → NotificationService.reset() │ └── notifyListeners() └── 导航回主页面 ``` ### 4.2 编辑习惯 ``` 用户点击习惯卡片标题区域 │ └── AppStateManager.goEditHabit(true) │ └── EditHabitScreen(isNew: false, habit: currentHabit) │ ├── 表单预填充现有数据 ├── 额外按钮: │ ├── 归档/取消归档 (FAB 左侧) │ └── 删除 (AppBar) │ └── 直接删除,无确认对话框 │ └── 点击保存: ├── 更新 HabitData 字段 ├── HabitsManager.editHabit(habit) │ ├── HabitRepository.updateHabit() → UPDATE │ ├── CategoryRepository.updateHabitCategories() │ ├── NotificationService.reset() │ └── notifyListeners() └── 导航回主页面 ``` ### 4.3 删除习惯 ``` 用户在编辑页点击删除按钮 │ └── HabitsManager.deleteHabit(id) ├── 从内存列表中移除 ├── HabitRepository.deleteHabit() → DELETE FROM habits WHERE id = ? │ └── CASCADE DELETE events, habit_categories ├── NotificationService.removeNotifications(id) ├── HomeWidgetService.update() ├── UIFeedbackService.showMessageWithAction( │ "Habit deleted.", │ "Undo", │ () => undoDelete() │ ) └── notifyListeners() ``` ### 4.4 归档/取消归档 ``` 归档: HabitsManager.archiveHabit(id) ├── 更新 habit.archived = true ├── HabitRepository.updateHabit() ├── NotificationService.removeNotifications(id) ├── UIFeedbackService.showSuccess("Habit archived") └── notifyListeners() 取消归档: HabitsManager.unarchiveHabit(id) ├── 更新 habit.archived = false ├── HabitRepository.updateHabit() ├── 如果 habit.notification → NotificationService.reset() ├── UIFeedbackService.showSuccess("Habit unarchived") └── notifyListeners() ``` ### 4.5 拖拽排序 ``` 用户长按拖动习惯卡片到新位置 │ └── onReorder(oldIndex, newIndex) ├── 调整 newIndex (如果 oldIndex < newIndex → newIndex--) ├── 列表操作: list.removeAt(oldIndex), list.insert(newIndex, item) ├── 更新所有习惯的 position 字段 ├── HabitsManager.reorderList(oldIndex, newIndex) │ ├── 更新内存列表 │ ├── HabitRepository.updateHabitsOrder() → UPDATE position │ └── notifyListeners() └── UI 自动刷新 ``` --- ## 5. 数值型习惯进度输入 ### 5.1 ProgressInputModal 交互 ``` 弹出 ProgressInputModal │ ├── 显示: │ ├── 标题: "Save Progress" │ ├── 圆形进度指示器 (120px 直径) │ │ ├── 未完成: 显示百分比 (如 "70%") │ │ ├── 已完成: 显示 ✓ 图标 │ │ └── 超出: 显示百分比 (> 100%) │ ├── 当前值 / 目标值 显示 │ └── 控制按钮行: │ ├── [-] 减少 partialValue │ ├── [+] 增加 partialValue │ └── [Complete] 直接设为 targetValue │ ├── 用户操作: │ ├── 点击 [+]: │ │ ├── currentValue += partialValue │ │ └── clamp(0, targetValue * 2) │ ├── 点击 [-]: │ │ ├── currentValue -= partialValue │ │ └── clamp(0, targetValue * 2) │ ├── 点击 Complete: │ │ └── currentValue = targetValue │ ├── 点击当前值: │ │ └── 弹出文本输入框直接编辑 │ └── 点击 Save: │ ├── 确定 DayType: │ │ ├── currentValue >= targetValue → DayType.check │ │ └── currentValue < targetValue → DayType.progress │ ├── 创建事件: [DayType, "", currentValue, targetValue] │ └── 返回事件数据给调用方 │ └── 点击 Cancel → 返回 null ``` --- ## 6. 日历组件行为 ### 6.1 日历格式 ``` 月视图 → 显示整月 周视图 → 显示一周 切换触发: ├── 用户点击日历标题区域 → 切换格式 └── 月份切换时重置为月视图 ``` ### 6.2 日历日期标记 ``` 每个日期根据当天的 event[0] 显示不同颜色的圆点: DayType.check → checkColor (默认 #09BF30 绿色) DayType.fail → failColor (默认 #F44336 红色) DayType.skip → skipColor (默认 #FBC02D 黄色) DayType.progress → progressColor (默认 #2196F3 蓝色) DayType.clear → 无标记 今天特殊显示: → 外圈高亮环 ``` ### 6.3 月份名称显示 ``` 如果 showMonthName 设置为 true: → 在日历上方显示当前月份名称文本 如果 false: → 不显示 ``` --- ## 7. 设置项完整规格 ### 7.1 所有设置项及默认值 | 设置项 | 类型 | 默认值 | 持久化 Key | |--------|------|--------|------------| | theme | Themes 枚举 | Themes.device | theme | | weekStart | StartingDayOfWeek | monday | weekStart | | showDailyNot | bool | true | showDailyNot | | dailyNotTime | TimeOfDay | 20:00 | dailyNotTime | | soundEffects | bool | true | soundEffects | | soundVolume | double | 3.0 (范围 0-5) | soundVolume | | biometricLock | bool | false | biometricLock | | oneTapCheck | bool | false | oneTapCheck | | showMonthName | bool | true | showMonthName | | showCategories | bool | true | showCategories | | seenOnboarding | bool | false | seenOnboarding | | lastWhatsNewVersion | String | '' | lastWhatsNewVersion | | checkColor | Color | #09BF30 | checkColor | | failColor | Color | #F44336 | failColor | | skipColor | Color | #FBC02D | skipColor | | progressColor | Color | #2196F3 | progressColor | ### 7.2 设置项分组 (UI 展示顺序) ``` 外观 (Appearance): ├── Theme (下拉: Device / Light / Dark / OLED / Material You) ├── First day of the week (下拉: Su Mo Tu We Th Fr Sa) ├── Show month name (开关) ├── Show categories (开关) └── Set colors (点击打开颜色选择器 × 4) 通知 (Notifications): ├── App notifications (开关) → 控制每日提醒 └── Notification time (时间选择器, 仅通知开启时可用) 音效 (Sound): └── Sound effects (滑块 0-5, 左侧图标, 右侧数字) 安全 (Security): ├── Biometric Lock (开关, 需设备支持) └── Single tap to check (开关) 数据管理: ├── Backup → Create (导出 JSON 文件) ├── Backup → Restore (导入 JSON 文件) ├── Onboarding (重播引导) └── What's New (查看更新日志) 关于: ├── App 名称 + 版本号 ├── Terms and Conditions (URL) ├── Privacy Policy (URL) ├── Source code (GitHub URL) └── Support (捐赠 URL) ``` --- ## 8. 备份/恢复流程 ### 8.1 创建备份 ``` 用户点击 "Create" 备份按钮 │ └── BackupService.createDatabaseBackup() │ ├── 1. 从 HabitsManager 获取所有习惯 ├── 2. 序列化每个习惯: │ ├── id, position, title, twoDayRule, cue, routine, reward │ ├── showReward, advanced, notification, notTime │ ├── sanction, showSanction, accountant │ ├── habitType, targetValue, partialValue, unit │ └── events: { "YYYY-MM-DD": [dayTypeIndex, ...] } ├── 3. 序列化分类: │ └── categories: [{ id, title, iconCodePoint, fontFamily }] ├── 4. 序列化关联: │ └── habit_categories: [{ habit_id, category_id }] ├── 5. 添加元数据: │ └── metadata: { import_timestamp, version } ├── 6. JSON.encode → 字符串 ├── 7. 弹出文件保存对话框 (flutter_file_dialog / file_picker) └── 8. 写入文件 ├── 成功 → UIFeedbackService.showSuccess("Backup created successfully!") └── 失败 → UIFeedbackService.showError("Backup failed!") ``` ### 8.2 恢复备份 ``` 用户点击 "Restore" 备份按钮 │ └── 弹出确认对话框: "All habits will be replaced with habits from backup." │ ├── Cancel → 取消 │ └── Restore → │ ├── BackupService.loadBackup() │ │ │ ├── 1. 弹出文件选择对话框 │ ├── 2. 读取文件内容 │ ├── 3. 验证: │ │ ├── 文件是否存在 │ │ ├── 文件大小 <= 10MB │ │ └── JSON 格式是否有效 │ ├── 4. 解析 JSON: │ │ ├── 如果是数组 → 旧版格式,转换为新格式 │ │ └── 如果是对象 → 新版格式 │ ├── 5. 清空数据库: │ │ ├── DELETE FROM habits │ │ └── DELETE FROM events │ ├── 6. 逐个插入习惯和事件 │ └── 7. 插入分类和关联 │ └── 成功后: ├── HabitsManager.loadHabits() ← 重新加载 ├── NotificationService.reset() ← 重置通知 ├── HomeWidgetService.update() ← 更新小组件 └── UIFeedbackService.showSuccess("Restore completed successfully!") ``` --- ## 9. 引导页流程 ``` 首次启动应用 │ └── SettingsManager.seenOnboarding == false │ └── 显示 OnboardingScreen │ ├── Step 1: "Define your habits" │ ├── 插图: empty_list.svg │ ├── 描述: "To better stick to your habits, you can define:" │ ├── 概念 1: Cue (提示触发器) │ ├── 概念 2: Routine (例行动作) │ └── 概念 3: Reward (奖励) │ ├── Step 2: "Log your days" │ ├── 插图: habit_tracking.svg │ └── 每日操作: │ ├── ✓ Successful (完成) │ ├── + Progressive (进度) │ ├── ✗ Not so successful (失败) │ ├── ⏭ Skip (跳过) │ └── 💬 Note (备注) │ ├── Step 3: "Observe your progress" │ ├── 插图: progress.svg │ └── 描述: "You can track your progress through the calendar │ view in every habit or on the statistics page." │ ├── 导航: Skip (跳过) / Next (下一步) / Done (完成) │ └── 完成: ├── SettingsManager.seenOnboarding = true ├── SettingsManager.saveData() └── 导航到 HabitsScreen ``` --- ## 10. 生物识别认证流程 ``` 应用启动 或 从后台恢复 (AppLifecycleState.resumed) │ └── BiometricAuthWrapper │ ├── 检查 biometricLock 设置 │ ├── false → 直接显示内容 │ └── true → │ │ │ ├── BiometricAuthService.authenticate() │ │ ├── 获取可用生物识别类型 │ │ │ ├── 指纹 → "Fingerprint" │ │ │ ├── 面容 → "Face ID" │ │ │ ├── 虹膜 → "Iris" │ │ │ └── 设备凭据 → "Device PIN, Pattern, or Password" │ │ │ │ │ └── local_auth.authenticate( │ │ localizedReason: "Please authenticate to access Habo", │ │ biometricOnly: false │ │ ) │ │ │ ├── 成功 → 显示内容 │ │ │ └── 失败 → 显示认证错误界面: │ ├── 标题: "Authentication Required" │ ├── 描述: "Please authenticate to access Habo" │ ├── 图标: 指纹图标 │ └── "Try Again" 按钮 → 重新认证 │ └── 设备不支持生物识别: ├── showToast: "Please set up fingerprint/face unlock" └── 自动关闭 biometricLock 设置 ``` --- ## 11. 桌面小组件数据更新 ``` 习惯事件变化时 │ └── HomeWidgetService.update() │ ├── 1. 计算今日习惯进度: │ ├── 获取所有活跃习惯 │ ├── 计算今日已完成数 (DayType.check 或 progress >= target) │ └── 计算总习惯数 │ ├── 2. 写入小组件数据: │ ├── HomeWidget.saveWidgetData("habitsCompleted", count) │ ├── HomeWidget.saveWidgetData("habitsTotal", total) │ └── HomeWidget.updateWidget() │ └── 3. 小组件渲染: ├── CircularProgressPainter │ └── 弧度 = (completed / total) * 2π ├── 中心显示: "completed / total" └── 尺寸: 170 × 170 ``` --- ## 12. 通知调度逻辑 ``` NotificationService.resetNotifications(): │ ├── 1. 取消所有现有通知: │ └── AwesomeNotifications().cancelAll() │ ├── 2. 检查全局通知开关: │ └── if !showDailyNot → return │ └── 3. 遍历所有活跃习惯: └── if habit.notification: ├── 创建每日重复通知: │ ├── id: habit.id │ ├── channel: "habit_notifications" │ ├── title: "Habo" │ ├── body: "Do not forget to check your habits." │ ├── schedule: 每日 at habit.notTime │ └── payload: { habitId: habit.id } │ └── AwesomeNotifications().createNotification() ``` --- ## 13. 完整数据库 Schema ### 13.1 habits 表 ```sql CREATE TABLE habits ( id INTEGER PRIMARY KEY AUTOINCREMENT, position INTEGER, -- 排序权重 title TEXT NOT NULL, -- 习惯标题 (必填) twoDayRule INTEGER DEFAULT 0, -- 两天法则 0=关 1=开 cue TEXT DEFAULT '', -- 提示触发器 routine TEXT DEFAULT '', -- 例行动作 reward TEXT DEFAULT '', -- 奖励 showReward INTEGER DEFAULT 0, -- 显示奖励 0=关 1=开 advanced INTEGER DEFAULT 0, -- 高级模式 0=关 1=开 notification INTEGER DEFAULT 0, -- 通知开关 0=关 1=开 notTime TEXT DEFAULT '', -- 通知时间 "HH:MM" sanction TEXT DEFAULT '', -- 惩罚描述 showSanction INTEGER DEFAULT 0, -- 显示惩罚 0=关 1=开 accountant TEXT DEFAULT '', -- 问责伙伴 habitType INTEGER DEFAULT 0, -- 0=布尔 1=数值 targetValue REAL DEFAULT 1.0, -- 目标值 partialValue REAL DEFAULT 1.0, -- 部分增量 unit TEXT DEFAULT '', -- 单位 archived INTEGER DEFAULT 0 -- 0=活跃 1=归档 ); ``` ### 13.2 events 表 ```sql CREATE TABLE events ( id INTEGER NOT NULL, -- FK → habits.id dateTime TEXT NOT NULL, -- ISO8601 日期字符串 dayType INTEGER NOT NULL, -- 0=clear 1=check 2=fail 3=skip 4=progress comment TEXT DEFAULT '', -- 备注 progressValue REAL DEFAULT 0.0, -- 进度值 (数值型) targetValue REAL DEFAULT 0.0, -- 目标值快照 (数值型) PRIMARY KEY (id, dateTime), FOREIGN KEY (id) REFERENCES habits(id) ON DELETE CASCADE ); ``` ### 13.3 categories 表 ```sql CREATE TABLE categories ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, -- 分类名称 iconCodePoint INTEGER NOT NULL, -- IconData.codePoint fontFamily TEXT -- 字体族 (如 fontAwesomeFlutter) ); ``` ### 13.4 habit_categories 关联表 ```sql CREATE TABLE habit_categories ( habit_id INTEGER NOT NULL, category_id INTEGER NOT NULL, PRIMARY KEY (habit_id, category_id), FOREIGN KEY (habit_id) REFERENCES habits(id) ON DELETE CASCADE, FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE CASCADE ); ``` --- ## 14. 跨日自动刷新 ``` 应用启动时: └── _startDayChangeTimer() └── Timer.periodic(Duration(hours: 1), callback) │ ├── 记录当前日期: lastDate = DateTime.now().day │ └── 每小时检查: ├── if DateTime.now().day != lastDate: │ ├── lastDate = DateTime.now().day │ ├── HabitsManager.loadHabits() ← 重载数据 │ ├── NotificationService.reset() ← 重置通知 │ └── HomeWidgetService.update() ← 更新小组件 │ └── else → 无操作 应用暂停时: └── _stopDayChangeTimer() ← 暂停定时器 应用恢复时: └── 检查日期变化 → 刷新 → _startDayChangeTimer() ← 恢复定时器 ``` --- ## 15. 颜色选择器交互 ``` 用户点击颜色设置项 │ └── 弹出 ColorIcon 对话框 │ ├── 显示 HueRingPicker: │ ├── 色相环 (360°) │ └── 饱和度/亮度选择区域 │ ├── 当前选中颜色的实时预览 ├── 重置按钮 → 恢复默认颜色 │ └── 确认 → SettingsManager 更新颜色值 ├── SharedPreferences 保存 (ARGB int) └── notifyListeners() → 全局主题刷新 ```