- Complete MVP with Repository Pattern, SQLite storage - Provider + ChangeNotifier state management - Navigation 2.0 with deep link support - Habit CRUD with twoDayRule, notifications, categories - Backup/Restore via JSON - Statistics with streak tracking - Material You theme support - Biometric lock support - Desktop widget support - 27 languages i18n structure - Comprehensive test suite (87/89 passing)
30 KiB
30 KiB
Habo 实现规格文档
基于 REQUIREMENTS.md 和 ARCHITECTURE.md,定义每个功能的具体实现逻辑、算法和交互流程
1. 连续天数 (Streak) 算法
1.1 普通模式 (_updateLastStreakNormal)
输入: habitData.events (SplayTreeMap<DateTime, List>, 按日期升序排列)
输出: 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 事件数据结构
// 布尔型习惯
[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 数据结构
class StatisticsData {
String title; // 习惯标题
int topStreak = 0; // 最高连续天数
int actualStreak = 0; // 当前连续(遍历中)
int checks = 0; // 完成次数
int fails = 0; // 失败次数
int skips = 0; // 跳过次数
int progress = 0; // 进度次数
SplayTreeMap<int, Map<DayType, List<int>>> 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<int>("habitsCompleted", count)
│ ├── HomeWidget.saveWidgetData<int>("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 表
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 表
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 表
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 关联表
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() → 全局主题刷新