- 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)
808 lines
30 KiB
Markdown
808 lines
30 KiB
Markdown
# 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 事件数据结构
|
||
|
||
```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<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 表
|
||
|
||
```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() → 全局主题刷新
|
||
```
|