Compare commits

..

5 Commits

Author SHA1 Message Date
ytc1012
86a368e1e3 优化 2025-12-02 16:38:58 +08:00
ytc1012
ef44d11c32 优化 2025-11-27 18:30:49 +08:00
ytc1012
15252dfd88 update md 2025-11-27 14:00:13 +08:00
ytc1012
5dccf27059 del md 2025-11-27 13:40:21 +08:00
ytc1012
58f6ec39b7 积分、成就系统 2025-11-27 13:37:10 +08:00
51 changed files with 8874 additions and 1895 deletions

View File

@@ -49,7 +49,20 @@
"Bash(if [ -d \"android/app/build/outputs\" ])", "Bash(if [ -d \"android/app/build/outputs\" ])",
"Bash(then find android/app/build/outputs -type f ( -name \"*.aab\" -o -name \"*.apk\" ))", "Bash(then find android/app/build/outputs -type f ( -name \"*.aab\" -o -name \"*.apk\" ))",
"Bash(else echo \"outputs 目录不存在,可能还未构建过\")", "Bash(else echo \"outputs 目录不存在,可能还未构建过\")",
"Bash(fi)" "Bash(fi)",
"Bash(tasklist:*)",
"Bash(flutter pub outdated:*)",
"Bash(find:*)",
"Bash(java:*)",
"Bash(mkdir:*)",
"Bash(keytool:*)",
"Bash(call android\\gradlew.bat:*)",
"Bash(./android/gradlew.bat:*)",
"Bash(\"F:\\Program Files\\Eclipse Adoptium\\jdk-17.0.17.10-hotspot\\bin\\keytool.exe\":*)",
"Bash(\"F:\\Program Files\\Eclipse Adoptium\\jdk-17.0.17.10-hotspot\\bin\\jarsigner.exe\":*)",
"Bash(del:*)",
"Bash(rm:*)",
"Bash(git add:*)"
], ],
"deny": [], "deny": [],
"ask": [] "ask": []

View File

@@ -1,373 +0,0 @@
# FocusBuddy 图标设计教程 - Figma 完整指南
> **设计目标**: 制作"温柔专注伙伴"图标 (Design 1: Gentle Focus Buddy)
> **工具**: Figma (免费版即可)
> **时间**: 约 30-45 分钟
> **难度**: ⭐⭐☆☆☆ (适合初学者)
---
## 📋 准备工作
### 1. 注册 Figma 账号
1. 访问 [figma.com](https://www.figma.com)
2. 点击右上角 **"Sign up"** 注册免费账号
3. 可以使用 Google 账号快速登录
### 2. 创建新文件
1. 登录后点击左上角 **"+ New design file"**
2. 等待加载完成,进入空白画布
---
## 🎨 第一步: 创建画布和背景
### 1.1 创建 1024×1024 画布
1. **创建 Frame (画布框架)**
- 按键盘 `F` 键 (或点击顶部工具栏的方框图标)
- 在右侧面板找到 **"Frame"** 区域
-**"W"** (宽度) 输入 `1024`
-**"H"** (高度) 输入 `1024`
- 在画布上点击创建
2. **重命名 Frame**
- 双击左侧图层面板的 "Frame 1"
- 重命名为 `FocusBuddy Icon`
### 1.2 添加圆角矩形背景
1. **创建背景矩形**
- 按键盘 `R` 键选择矩形工具
- 在 Frame 内点击并拖动,创建一个完全覆盖 Frame 的矩形
- 在右侧面板确保尺寸是:
- **W**: `1024`
- **H**: `1024`
- **X**: `0`
- **Y**: `0`
2. **添加圆角**
- 选中矩形
- 在右侧找到 **"Corner radius"** (圆角半径)
- 输入 `180`
3. **添加渐变色**
- 选中矩形
- 在右侧 **"Fill"** 区域,点击颜色方块
- 点击 **"Solid"** 下拉菜单,选择 **"Linear"** (线性渐变)
- 调整渐变:
- **顶部颜色** (第一个色标): `#A7C4BC`
- **底部颜色** (第二个色标): `#88C9A1`
- 确保渐变方向是从上到下 (拖动渐变线可调整)
4. **重命名图层**
- 在左侧图层面板,双击矩形图层
- 重命名为 `Background`
---
## 🔵 第二步: 创建外圆环
### 2.1 绘制外圆环
1. **创建圆形**
- 按键盘 `O` 键 (或点击工具栏椭圆工具)
- 按住 `Shift` 键,在画布中心拖动创建正圆
- 在右侧面板设置:
- **W**: `800` (半径 400 × 2)
- **H**: `800`
-`Option/Alt + H` 水平居中
-`Option/Alt + V` 垂直居中
2. **设置为描边样式**
- 选中圆形
- 点击右侧 **"Fill"** 右边的 `-` 号删除填充
- 点击 **"Stroke"** 旁边的 `+` 号添加描边
- 点击描边颜色,输入 `#F8F6F2`
- 设置描边粗细:
-**"Stroke"** 下方输入 `60`
- 设置透明度:
- 在右侧顶部找到 **"Layer"** 区域
- **"Opacity"** (不透明度) 调整为 `90%`
3. **重命名图层**
- 重命名为 `Outer Ring`
---
## ⚪ 第三步: 创建内圆
### 3.1 绘制内圆
1. **创建圆形**
- 按键盘 `O`
- 按住 `Shift` 创建正圆
- 设置尺寸:
- **W**: `560` (半径 280 × 2)
- **H**: `560`
- 居中对齐: `Option/Alt + H``Option/Alt + V`
2. **设置填充色**
- 选中圆形
- **"Fill"**: `#F8F6F2`
- **"Opacity"**: `95%`
3. **重命名图层**
- 重命名为 `Inner Circle`
---
## 😊 第四步: 绘制友好的笑脸
### 4.1 绘制左眼
1. **创建圆形**
-`O` 键创建圆形
- 设置尺寸:
- **W**: `48` (半径 24 × 2)
- **H**: `48`
- 设置位置 (相对于整个 Frame):
- **X**: `428` (中心点 452 - 半径 24)
- **Y**: `456` (中心点 480 - 半径 24)
2. **设置颜色**
- **"Fill"**: `#5B6D6D`
- **"Opacity"**: `70%`
3. **重命名**: `Left Eye`
### 4.2 绘制右眼
1. **复制左眼**
- 选中左眼
-`Cmd/Ctrl + D` 复制
- 设置新位置:
- **X**: `548` (中心点 572 - 半径 24)
- **Y**: 保持 `456`
2. **重命名**: `Right Eye`
### 4.3 绘制微笑曲线
1. **使用钢笔工具**
- 按键盘 `P` 键 (钢笔工具)
- 在画布上点击三个点创建曲线:
- **起点**: X: `432`, Y: `560`
- **中间控制点**: X: `512`, Y: `600`
- **终点**: X: `592`, Y: `560`
2. **调整为平滑曲线**
- 选中钢笔工具创建的路径
- 按键盘 `Enter` 键进入编辑模式
- 选中中间的点,在顶部工具栏点击 **"Bend tool"** (弯曲工具)
- 向下拖动中间点,创建向下的弧形
**提示**: 如果钢笔工具太复杂,可以使用简化方法:
- 创建椭圆 (W: `160`, H: `80`)
- 位置: X: `432`, Y: `560`
- 选中椭圆,按 `Enter` 进入编辑模式
- 选中顶部两个点并删除 (保留底部半圆)
- 旋转 180° 形成微笑
3. **设置描边样式**
- 删除填充 (点击 Fill 的 `-`)
- 添加描边 (点击 Stroke 的 `+`)
- **"Stroke"** 颜色: `#5B6D6D`
- **"Stroke"** 粗细: `16`
- **"Opacity"**: `70%`
- 在描边选项中,选择 **"Round cap"** (圆形端点)
4. **重命名**: `Smile`
---
## 🎯 第五步: 添加中心点 (可选装饰)
### 5.1 绘制中心圆点
1. **创建小圆**
-`O`
- 创建圆形:
- **W**: `80` (半径 40 × 2)
- **H**: `80`
- 居中对齐
2. **设置颜色**
- **"Fill"**: `#A7C4BC`
- **"Opacity"**: `30%`
3. **重命名**: `Center Dot`
---
## 📐 第六步: 整理图层结构
### 6.1 调整图层顺序
在左侧图层面板,从上到下的顺序应该是:
```
FocusBuddy Icon (Frame)
├── Center Dot
├── Smile
├── Right Eye
├── Left Eye
├── Inner Circle
├── Outer Ring
└── Background
```
如果顺序不对,拖动图层调整位置。
### 6.2 创建图层分组 (可选)
1. **选中所有脸部元素**
- 按住 `Shift` 点击: Left Eye, Right Eye, Smile
- 右键点击,选择 **"Group selection"**
- 重命名为 `Face`
---
## 💾 第七步: 导出图标
### 7.1 导出 1024×1024 PNG
1. **选中整个 Frame**
- 点击左侧图层面板的 `FocusBuddy Icon`
2. **设置导出选项**
- 在右侧底部找到 **"Export"** 区域
- 点击 `+` 号添加导出设置
- 格式选择 **"PNG"**
- 倍数选择 **"1x"**
- 点击 **"Export FocusBuddy Icon"** 按钮
- 选择保存位置,保存为 `focusbuddy-icon-1024.png`
### 7.2 生成所有尺寸
1. **访问 AppIcon.co**
- 打开浏览器访问 [https://www.appicon.co](https://www.appicon.co)
2. **上传图标**
- 点击 **"Choose File"** 或拖动刚才导出的 PNG 文件
- 等待处理完成
3. **下载图标包**
- 点击 **"Download"** 按钮
- 会下载包含 iOS 和 Android 所有尺寸的图标包
---
## 🎨 调整和优化技巧
### 微调笑脸表情
如果觉得笑容太大或太小:
- 选中 `Smile` 图层
- 调整起点和终点的 Y 坐标 (往上=笑容变小,往下=笑容变大)
- 或调整中间控制点的 Y 坐标 (往上=笑容变小,往下=笑容变大)
### 调整眼睛位置
如果觉得眼睛间距太宽或太窄:
- 选中两只眼睛
- 使用键盘方向键微调位置
- 或直接修改 X 坐标值
### 改变颜色主题
如果想尝试其他配色:
1. 选中 `Background`
2. 修改渐变颜色
3. 保持柔和的莫兰迪色系风格
---
## ✅ 检查清单
完成后,检查以下项目:
- [ ] Frame 尺寸是 1024×1024
- [ ] 背景圆角半径是 180
- [ ] 背景渐变从 #A7C4BC#88C9A1
- [ ] 外圆环描边粗细 60,颜色 #F8F6F2,透明度 90%
- [ ] 内圆填充 #F8F6F2,透明度 95%
- [ ] 两只眼睛大小相同,对称分布
- [ ] 微笑曲线居中,端点圆润
- [ ] 导出的 PNG 文件清晰无锯齿
---
## 🚀 下一步
1. **应用到 Flutter 项目**
- 解压 AppIcon.co 下载的文件
- iOS: 将 `AppIcon.appiconset` 文件夹放到 `ios/Runner/Assets.xcassets/`
- Android: 将对应尺寸的图标放到 `android/app/src/main/res/` 各个 `mipmap-*` 文件夹
2. **测试效果**
- 在真机或模拟器上运行应用
- 检查桌面图标显示效果
---
## 🆘 常见问题
### Q: 钢笔工具太难用怎么办?
**A**: 使用椭圆工具替代:
1. 创建椭圆 W: 160, H: 80
2. 按 Enter 进入编辑模式
3. 删除顶部两个锚点
4. 保留底部弧线即可
### Q: 图层无法居中?
**A**: 确保选中了 Frame 内的图层,然后:
- Mac: `Option + H` (水平居中), `Option + V` (垂直居中)
- Windows: `Alt + H`, `Alt + V`
- 或使用右侧 **"Alignment"** 对齐工具
### Q: 导出的图标边缘有白边?
**A**: 确保 Background 图层完全覆盖了 Frame,且没有透明间隙。
### Q: 颜色看起来不够柔和?
**A**: 检查所有元素的不透明度 (Opacity) 设置,适当降低可以让设计更柔和。
---
## 📚 扩展学习
### Figma 快捷键
| 功能 | Mac | Windows |
|------|-----|---------|
| 矩形 | R | R |
| 圆形 | O | O |
| 钢笔 | P | P |
| Frame | F | F |
| 复制 | Cmd + D | Ctrl + D |
| 水平居中 | Option + H | Alt + H |
| 垂直居中 | Option + V | Alt + V |
| 缩放视图 | Cmd + 滚轮 | Ctrl + 滚轮 |
### 推荐资源
- [Figma 官方教程](https://help.figma.com/hc/en-us/categories/360002051613-Get-started) (中文)
- [Figma 中文社区](https://www.figma.cool/)
- [YouTube: Figma 入门教程](https://www.youtube.com/results?search_query=figma+tutorial+chinese)
---
## 🎉 完成!
恭喜您完成了 FocusBuddy 图标的设计!这个图标传达了:
-**温柔友好** - 柔和的颜色和圆润的形状
- 😊 **情感支持** - 友好的笑脸象征陪伴
- 🎯 **专注** - 同心圆代表专注的中心
- 🌿 **无压力** - 莫兰迪色系带来平静感
如果有任何问题,欢迎随时提问!
---
**文档版本**: 1.0
**最后更新**: 2025年11月24日
**作者**: FocusBuddy 开发团队

362
README.md
View File

@@ -1,174 +1,131 @@
# FocusBuddy 产品优化总结 # FocusBuddy 产品实现总结
**优化日期**: 2025年11月22 **实现日期**: 2025年11月27
**目标**: 打造一个 4 周内可上线的 MVP 版本 **状态**: 已完成 MVP 版本开发
**策略**: 删繁就简,聚焦核心价值 **核心价值**: 无惩罚专注,温柔回归
--- ---
## 📂 新增文档清单 ## 📂 文档清单
已为你创建以下完整的产品文档:
| 文档 | 路径 | 用途 | | 文档 | 路径 | 用途 |
|------|------|------| |------|------|------|
| ✅ 产品设计 | [product-design.md](product-design.md) | 原始完整方案 | | ✅ 产品设计 | [product-design.md](product-design.md) | 产品理念和市场定位 |
| ✅ UI 设计规范 | [ui-design-spec.md](ui-design-spec.md) | 完整的 UI/UX 细节(已补全) | | ✅ UI 设计规范 | [ui-design-spec.md](ui-design-spec.md) | UI/UX 设计细节 |
| ✅ 隐私政策 | [privacy-policy.md](privacy-policy.md) | 需填写开发者信息 | | ✅ 隐私政策 | [privacy-policy.md](privacy-policy.md) | 隐私保护声明 |
| ✅ **MVP 上线清单** | [mvp-launch-checklist.md](mvp-launch-checklist.md) | **核心文档!必读** | | ✅ 应用商店文案 | [app-store-metadata.md](app-store-metadata.md) | 上架时直接复制使用 |
| ✅ **应用商店文案** | [app-store-metadata.md](app-store-metadata.md) | 上架时直接复制使用 |
| ✅ 服务条款 | [terms-of-service.md](terms-of-service.md) | 上架必须项 | | ✅ 服务条款 | [terms-of-service.md](terms-of-service.md) | 上架必须项 |
--- ---
## 🎯 核心优化建议汇总 ## 🎯 已实现核心功能
### 1. 功能精简(最重要) ### 1. 页面功能
#### 从原方案删除/延后的功能: | 页面 | 功能 | 说明 |
|------|------|------|
| **Home** | 一键开始专注 | 显示积分卡片、应用标题、时长选择、开始专注按钮和底部导航 |
| **Focus** | 专注计时 | 显示计时器、分心按钮和暂停按钮 |
| **Complete** | 专注完成 | 显示专注结果、鼓励文案和"Start Another"按钮 |
| **History** | 历史记录 | 显示当天记录列表,支持查看详情 |
| **Settings** | 设置选项 | 包含默认时长选项、语言选择和隐私政策链接 |
| **Profile** | 个人资料 | 显示积分、等级和连续签到记录 |
| **Onboarding** | 引导页 | 解释"无惩罚"理念,降低用户困惑 |
| 原功能 | 决策 | 原因 | ### 2. 核心功能
|--------|------|------|
| ⏸️ 时长滑动条5-60分钟 | **延后到 V1.1** | 固定 25 分钟降低复杂度 |
| ⏸️ 白噪音播放 | **延后到 V1.1** | 需要音频资源 + 测试成本高 |
| ⏸️ PDF 报告导出 | **延后到 V1.2** | 用户需求未验证,开发成本高 |
| ⏸️ Lottie 动画 | **简化为静态** | 节省 3 天开发时间 |
| ⏸️ 主题皮肤系统 | **改用文字徽章** | 设计成本太高 |
| ⏸️ 每周趋势图表 | **仅显示今日** | 图表库集成复杂 |
| ❌ TopOn 广告聚合 | **删除** | AdMob 已足够,过度优化 |
| ❌ Body Doubling Lite | **删除** | 概念模糊,非核心价值 |
#### MVP 保留的核心功能3 个页面): | 功能 | 说明 |
|------|------|
**Home** - 一键开始专注25 分钟固定) | **无惩罚机制** | 分心不中断计时,不断连成就,不重置进度 |
**Focus** - "I got distracted" 按钮 + 4 种分心分类 | **分心记录** | "I got distracted"按钮 + 4种分心分类(社交媒体、被打断、感到压力、走神) |
**Complete** - 今日统计 + 鼓励文案 + "Start Another" | **温柔鼓励** | 随机显示15条鼓励文案如"Showing up is half the battle" |
| **本地存储** | 使用Hive进行数据存储所有数据仅存于设备 |
**附加简化页面:** | **多语言支持** | 支持14种语言英语、中文、日语、韩语、西班牙语、德语、法语、葡萄牙语、俄语、印地语、印度尼西亚语、意大利语、阿拉伯语 |
- History仅显示当天记录列表 | **通知功能** | 后台计时通知,提醒用户正在计时中 |
- Settings默认时长 3 选项 + 隐私政策链接) | **积分系统** | 完成专注获得积分,提升等级 |
| **提前停止确认** | 点击Stop时友好提示防止误操作 |
| **空状态提示** | History页无数据时引导用户 |
--- ---
### 2. 新增必要功能 ### 3. 技术栈实现
#### 原方案缺失的功能: **已集成依赖包:**
| 新增功能 | 优先级 | 开发时间 | 用途 |
|---------|--------|---------|------|
| **Onboarding 引导页** | P0 | 1 天 | 解释"无惩罚"理念,降低用户困惑 |
| **空状态提示** | P0 | 0.5 天 | History 页无数据时引导用户 |
| **后台计时通知** | P1 | 0.5 天 | 切到后台时提醒"正在计时中" |
| **提前停止确认** | P1 | 0.5 天 | 点击 Stop 时友好提示 |
---
### 3. 技术栈优化
#### 依赖包精简(减少 4 个依赖):
**MVP 必须集成:**
```yaml ```yaml
dependencies: dependencies:
flutter: ^3.10.0-290.4.beta
flutter_localizations: ^0.1.0
cupertino_icons: ^1.0.8
hive: ^2.2.3 # 本地存储 hive: ^2.2.3 # 本地存储
hive_flutter: ^1.1.0 hive_flutter: ^1.1.0
flutter_local_notifications: ^17.0.0 # 通知 flutter_local_notifications: ^17.0.0 # 通知
path_provider: ^2.1.0 permission_handler: ^11.0.0 # 权限管理
path_provider: ^2.1.0 # 文件路径
shared_preferences: ^2.2.0 # 简单键值存储
intl: ^0.20.2 # 日期格式化和国际化
google_fonts: ^6.1.0 # Google Fonts (Nunito)
get_it: ^7.7.0 # 依赖注入框架
``` ```
**延后集成:** **开发工具:**
```yaml ```yaml
# workmanager: ^0.5.2 # 后台任务MVP 不需要) dev_dependencies:
# lottie: ^3.0.0 # 动画(用静态替代) flutter_test: ^0.0.0
# just_audio: ^0.9.36 # 音频(延后) flutter_lints: ^6.0.0
# pdf: ^3.10.0 # PDF导出延后 hive_generator: ^2.0.0 # Hive代码生成
# google_mobile_ads: ^4.0.0 # 广告V1.0.1 再加) build_runner: ^2.4.0 # 构建工具
``` ```
**节省开发时间**: 约 2-3 天 ---
## 📱 应用特点
### 1. 无惩罚机制
- 分心不中断计时
- 不断连成就
- 不重置进度
- 温柔鼓励文案
### 2. 本地优先
- 所有数据仅存于设备
- 不联网、不上传
- 保护用户隐私
### 3. 情绪友好
- 柔和的颜色搭配
- 清晰的字体设计
- 简单的交互流程
- 温暖的鼓励文案
### 4. 多语言支持
- 支持14种语言
- 本地化资源完整
- 支持动态切换语言
--- ---
### 4. 开发路线图调整 ## 🚀 上线准备
#### 原方案4 周,过于激进): ### 1. 应用商店准备
| 周数 | 原计划 | 风险 | **iOS App Store:**
|-----|--------|------| - [ ] 注册 Apple Developer 账号($99需 1-2 天审核)
| 第1周 | UI + 基础计时器 | ⚠️ 偏紧 |
| 第2周 | 分心记录 + Hive | ✅ 可行 |
| 第3周 | 报告 + 成就 | ⚠️ 功能太多 |
| 第4周 | 广告 + 测试 | ❌ 过于乐观 |
#### 优化后路线图4 周,更现实):
| 周数 | 目标 | 产出 |
|-----|------|------|
| **Week 1** | 核心框架 + 基础 UI | 能跑通 Home → Focus → Complete 流程 |
| **Week 2** | 数据存储 + 分心记录 | 能保存数据,能看到历史 |
| **Week 3** | 设置页 + 通知 + 真机测试 | 功能完整,可交给朋友测试 |
| **Week 4** | 上架准备 + 提交审核 | 提交 App Store & Play Store |
**详细每日任务**: 见 [mvp-launch-checklist.md](mvp-launch-checklist.md:275-347)
---
### 5. 上线前必备清单
#### 应用商店准备(⚠️ 提前做):
**iOS App Store ($99/年):**
- [ ] 注册 Apple Developer 账号(需 1-2 天审核)
- [ ] 准备 App 图标 1024×1024 - [ ] 准备 App 图标 1024×1024
- [ ] 准备 6.5" iPhone 截图(至少 3 张) - [ ] 准备 6.5" iPhone 截图(至少 3 张)
- [ ] 托管隐私政策GitHub Pages 免费) - [ ] 托管隐私政策GitHub Pages 免费)
- [ ] 填写应用描述(见 [app-store-metadata.md](app-store-metadata.md:7-106) - [ ] 填写应用描述(见 [app-store-metadata.md](app-store-metadata.md)
**Google Play Store ($25 一次性):** **Google Play Store:**
- [ ] 注册 Google Play Console 账号 - [ ] 注册 Google Play Console 账号$25立即生效
- [ ] 准备 App 图标 512×512 - [ ] 准备 App 图标 512×512
- [ ] 准备截图(至少 2 张) - [ ] 准备截图(至少 2 张)
- [ ] 填写应用描述(见 [app-store-metadata.md](app-store-metadata.md:110-191) - [ ] 填写应用描述(见 [app-store-metadata.md](app-store-metadata.md)
**合规文档(⚠️ 必须):** ### 2. 合规文档
- [ ] 填写 [privacy-policy.md](privacy-policy.md:4) 开发者信息
- [ ] 托管 [privacy-policy.md](privacy-policy.md) 到可访问的 URL
- [ ] 托管 [terms-of-service.md](terms-of-service.md) 到可访问的 URL - [ ] 托管 [terms-of-service.md](terms-of-service.md) 到可访问的 URL
- [ ] 创建支持邮箱: focusbuddy.support@gmail.com - [ ] 创建支持邮箱: focusbuddy.app@outlook.com
---
### 6. 商业化策略优化
#### 原方案问题:
- ❌ 首版就加广告 → 审核通过率低 + 用户体验差
- ❌ 主题皮肤变现 → 开发成本高,收益不确定
#### 优化后策略:
| 版本 | 变现方式 | 说明 |
|------|---------|------|
| **V1.0 (MVP)** | 完全免费,无广告 | 快速获取用户,验证产品价值 |
| **V1.1** | 激励视频广告 | 等下载量 > 1000 再加 |
| **V1.2** | IAP 去广告 $1.99 | 比原方案 $2.99 便宜,提高转化 |
| **V2.0** | Pro 订阅 $0.99/月 | 含白噪音 + PDF 报告等高级功能 |
**首月目标V1.0:**
- 下载量 > 500
- Day7 留存率 > 20%
- 如果达不到 → 说明产品体验有问题,需迭代核心功能
---
## ⚠️ 最大风险预警
### 可能导致项目失败的风险:
| 风险 | 概率 | 缓解措施 |
|------|------|---------|
| **iOS 审核被拒** | 60% | 不使用"ADHD 治疗"等医疗词汇,强调"productivity tool" |
| **真机测试发现严重 Bug** | 50% | 第 2 周即开始真机测试,不要等到最后 |
| **开发周期延误** | 40% | 严格遵守功能精简,抵制"再加一个小功能"的诱惑 |
| **产品留存率过低** | 30% | 如果 Day7 < 20%,说明核心价值不成立,需要重新思考 |
--- ---
@@ -179,131 +136,34 @@ dependencies:
| **下载量** | > 500 | App Store Connect / Play Console | | **下载量** | > 500 | App Store Connect / Play Console |
| **Day1 留存** | > 40% | 手动记录(对比首日下载 vs 次日活跃) | | **Day1 留存** | > 40% | 手动记录(对比首日下载 vs 次日活跃) |
| **Day7 留存** | > 20% | 同上 | | **Day7 留存** | > 20% | 同上 |
| **人均完成专注数** | > 3 次/周 | 后端分析(如果加了 Firebase | | **人均完成专注数** | > 3 次/周 | 本地数据统计 |
| **Crash 率** | < 2% | Firebase Crashlytics免费版 | | **Crash 率** | < 2% | Firebase Crashlytics免费版 |
| **评分** | > 4.0 | App Store / Play Store | | **评分** | > 4.0 | App Store / Play Store |
**如果指标不达标** → 说明产品体验有问题,需要: ---
1. 收集用户反馈(邮件 + Reddit 评论)
2. 分析流失环节(哪一步用户离开了?) ## 💡 产品亮点
3. 快速迭代核心功能
### 1. 无惩罚专注
- 传统番茄钟工具强调"完成",失败即惩罚
- FocusBuddy 允许分心,鼓励温柔回归
- 降低用户焦虑,提高持续使用意愿
### 2. 本地优先
- 所有数据仅存于设备,保护用户隐私
- 无需账号,无需联网,随时可用
- 适合注重隐私的用户
### 3. 情绪友好
- 柔和的颜色搭配,减少视觉刺激
- 温暖的鼓励文案,增强用户信心
- 简单的交互流程,降低使用门槛
### 4. 多语言支持
- 支持14种语言覆盖全球主要市场
- 本地化资源完整,提供良好的用户体验
--- ---
## 🚀 接下来的行动步骤 **文档状态:** ✅ 已完成 MVP 版本开发
**最后更新:** 2025年11月27日
### 立即执行(今天):
1. **阅读核心文档**
- [ ] 完整阅读 [mvp-launch-checklist.md](mvp-launch-checklist.md)
- [ ] 确认是否接受功能精简建议
- [ ] 理解 4 周开发路线图
2. **填写必要信息**
- [ ] 填写 [privacy-policy.md](privacy-policy.md:4) 中的开发者信息
- [ ] 修改 [terms-of-service.md](terms-of-service.md:96) 中的管辖地
- [ ] 决定是否需要创建网站托管文档
3. **账号准备**
- [ ] 注册 Apple Developer 账号($99需 1-2 天审核)
- [ ] 注册 Google Play Console 账号($25立即生效
- [ ] 创建支持邮箱: focusbuddy.support@gmail.com
### 第 1 周准备(开发前):
4. **开发环境**
- [ ] 安装 Flutter SDK稳定版 3.16+
- [ ] 配置 iOS / Android 开发环境
- [ ] 跑通 Hello World 项目
5. **设计资源**
- [ ] 设计 App 图标(或使用 Figma/Canva 模板)
- [ ] 如果不擅长设计,考虑花 $20-50 找 Fiverr 设计师
6. **最终确认**
- [ ] 确认产品名称FocusBuddy 还是备选?)
- [ ] 检查名称在 App Store / Play Store 是否可用
- [ ] 准备好 15 条鼓励文案(见 [ui-design-spec.md](ui-design-spec.md:710-726)
---
## 📚 文档使用指南
### 各文档用途:
| 阶段 | 使用文档 | 目的 |
|------|---------|------|
| **产品规划** | [product-design.md](product-design.md) | 理解产品理念和市场定位 |
| **开发阶段** | [ui-design-spec.md](ui-design-spec.md) | 参考所有 UI 组件、颜色、字体规范 |
| **开发阶段** | [mvp-launch-checklist.md](mvp-launch-checklist.md) | 每日对照开发任务,避免功能蔓延 |
| **上架准备** | [app-store-metadata.md](app-store-metadata.md) | 直接复制应用描述、关键词等 |
| **上架准备** | [privacy-policy.md](privacy-policy.md) + [terms-of-service.md](terms-of-service.md) | 托管到 GitHub Pages填写 URL |
| **推广阶段** | [app-store-metadata.md](app-store-metadata.md:195-251) | 使用 Reddit/TikTok 文案模板 |
---
## 💡 最后的建议
### 1. 抵制功能蔓延
**最大的风险是:边做边想"再加个小功能"**
坚持原则:
- ✅ 如果不影响核心价值("无惩罚专注"),就延后
- ❌ 不要因为"很简单"就加 → 累积起来会拖垮进度
### 2. 快速上线 > 完美产品
**4 周内必须提交审核**
记住:
- V1.0 不需要完美,只需要能用
- 真实用户反馈比你脑补重要 100 倍
- 上线后可以快速迭代
### 3. 保持核心差异化
**唯一不能妥协的:** "I got distracted" 按钮的体验
确保:
- 点击后真的不中断计时
- 鼓励文案真的让人感到温暖
- 没有任何"惩罚"的视觉暗示(红色、失败音效等)
### 4. 记录开发日志
建议每天记录:
- 今天完成了什么
- 遇到了什么困难
- 是否按计划推进
**好处:**
- 上线后可以写"How I Built This"推广文章
- 如果延期,能清楚知道时间花在哪了
---
## 🎯 最核心的一句话
> **"先做一个可上线的版本" = 只做让用户愿意回来的功能**
对 FocusBuddy 来说,这个功能就是:
**"I got distracted" 按钮 + 温柔的鼓励文案**
其他一切都是锦上添花。
---
**祝你开发顺利!** 🚀
有任何问题,随时继续问我。接下来你可以:
1. 开始注册开发者账号
2. 搭建 Flutter 环境
3. 按照 Week 1 的任务开始写代码
或者如果还有疑问,我可以帮你:
- 细化某一周的开发任务
- 写示例代码如计时器逻辑、Hive 数据结构)
- 优化应用商店描述文案
- 解答任何技术或产品问题
---
**文档状态:** ✅ 已完成所有优化建议
**最后更新:** 2025年11月22日

View File

@@ -1,3 +1,6 @@
import java.util.Properties
import java.io.FileInputStream
plugins { plugins {
id("com.android.application") id("com.android.application")
id("kotlin-android") id("kotlin-android")
@@ -5,6 +8,13 @@ plugins {
id("dev.flutter.flutter-gradle-plugin") id("dev.flutter.flutter-gradle-plugin")
} }
// Load keystore properties
val keystorePropertiesFile = rootProject.file("key.properties")
val keystoreProperties = Properties()
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(FileInputStream(keystorePropertiesFile))
}
android { android {
namespace = "com.focusbuddy.focus_buddy" namespace = "com.focusbuddy.focus_buddy"
compileSdk = flutter.compileSdkVersion compileSdk = flutter.compileSdkVersion
@@ -31,11 +41,18 @@ android {
versionName = flutter.versionName versionName = flutter.versionName
} }
signingConfigs {
create("release") {
keyAlias = keystoreProperties.getProperty("keyAlias")
keyPassword = keystoreProperties.getProperty("keyPassword")
storeFile = keystoreProperties.getProperty("storeFile")?.let { rootProject.file(it) }
storePassword = keystoreProperties.getProperty("storePassword")
}
}
buildTypes { buildTypes {
release { release {
// TODO: Add your own signing config for the release build. signingConfig = signingConfigs.getByName("release")
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug")
} }
} }
} }

View File

@@ -2,7 +2,7 @@
**Product:** FocusBuddy **Product:** FocusBuddy
**Version:** 1.0 (MVP) **Version:** 1.0 (MVP)
**Last Updated:** November 22, 2025 **Last Updated:** 2025年11月27日
--- ---

View File

@@ -121,5 +121,84 @@
"hindi": "हिन्दी", "hindi": "हिन्दी",
"indonesian": "Bahasa Indonesia", "indonesian": "Bahasa Indonesia",
"italian": "Italiano", "italian": "Italiano",
"arabic": "العربية" "arabic": "العربية",
"points": "النقاط",
"level": "المستوى",
"checked": "تم التسجيل",
"checkIn": "تسجيل الحضور",
"earnedPoints": "المكتسب:",
"basePoints": "النقاط الأساسية",
"honestyBonus": "مكافأة الصدق",
"totalPoints": "إجمالي النقاط: {count} ⚡",
"distractionsRecorded": "({count} {distractionText} مسجلة)",
"achievementUnlocked": "🎖️ إنجاز مفتوح!",
"bonusPoints": "+{points} نقاط ⚡",
"checkInSuccess": "تسجيل الحضور ناجح! +{points} نقاط ⚡",
"weeklyStreakBonus": "🎉 مكافأة السلسلة الأسبوعية!",
"newAchievementUnlocked": "🎖️ إنجاز جديد مفتوح!",
"alreadyCheckedIn": "لقد سجلت حضورك اليوم بالفعل! عد غدًا 📅",
"checkInCalendar": "تقويم تسجيل الحضور 📅",
"checkInToday": "📅 سجل الحضور اليوم",
"checkedInToday": "✓ تم التسجيل اليوم",
"currentStreak": "🔥 السلسلة الحالية",
"longestStreak": "🏆 أطول سلسلة",
"days": "أيام",
"daysCount": "{count} أيام",
"achievements": "الإنجازات 🎖️",
"viewAllAchievements": "عرض جميع الإنجازات",
"allAchievementsComingSoon": "شاشة الإنجازات الكاملة قريبًا!",
"profile": "الملف الشخصي",
"focuser": "المركز",
"pointsToNextLevel": "{points} نقاط إلى المستوى {level}",
"achievement_first_session_name": "مبتدئ التركيز",
"achievement_first_session_desc": "أكمل جلسة التركيز الأولى",
"achievement_sessions_10_name": "البداية",
"achievement_sessions_10_desc": "أكمل 10 جلسات تركيز",
"achievement_sessions_50_name": "عاشق التركيز",
"achievement_sessions_50_desc": "أكمل 50 جلسة تركيز",
"achievement_sessions_100_name": "سيد التركيز",
"achievement_sessions_100_desc": "أكمل 100 جلسة تركيز",
"achievement_honest_bronze_name": "المتتبع الصادق · برونزي",
"achievement_honest_bronze_desc": "سجل 50 تشتتًا بصدق",
"achievement_honest_silver_name": "المتتبع الصادق · فضي",
"achievement_honest_silver_desc": "سجل 200 تشتت بصدق",
"achievement_honest_gold_name": "المتتبع الصادق · ذهبي",
"achievement_honest_gold_desc": "سجل 500 تشتت بصدق",
"achievement_marathon_name": "عداء الماراثون",
"achievement_marathon_desc": "اجمع 10 ساعات من وقت التركيز",
"achievement_century_name": "نادي القرن",
"achievement_century_desc": "اجمع 100 ساعة من وقت التركيز",
"achievement_master_name": "جراند ماستر التركيز",
"achievement_master_desc": "اجمع 1000 ساعة من وقت التركيز",
"achievement_persistence_star_name": "نجمة المثابرة",
"achievement_persistence_star_desc": "سجل الحضور لمدة 7 أيام متتالية",
"achievement_monthly_habit_name": "العادة الشهرية",
"achievement_monthly_habit_desc": "سجل الحضور لمدة 30 يومًا متتاليًا",
"achievement_centurion_name": "المئوي",
"achievement_centurion_desc": "سجل الحضور لمدة 100 يوم متتالٍ",
"achievement_year_warrior_name": "محارب العام",
"achievement_year_warrior_desc": "سجل الحضور لمدة 365 يومًا متتاليًا",
"total": "الإجمالي",
"status": "الحالة",
"pointsBreakdown": "تفصيل النقاط",
"focusTimePoints": "وقت التركيز",
"focusTimePointsDesc": "نقطة واحدة لكل دقيقة تركيز",
"honestyBonusLabel": "مكافأة الصدق",
"honestyBonusDesc": "نقاط إضافية لتسجيل التشتتات",
"checkInPoints": "تسجيل الحضور اليومي",
"checkInPointsDesc": "النقاط الأساسية لتسجيل الحضور اليومي",
"streakBonus": "مكافأة السلسلة",
"streakBonusDesc": "{days} تسجيلات حضور متتالية",
"achievementBonusLabel": "مكافأة الإنجاز",
"weekdayS": "ح",
"weekdayM": "ن",
"weekdayT": "ث",
"weekdayW": "ر",
"weekdayTh": "خ",
"weekdayF": "ج",
"weekdaySa": "س"
} }

View File

@@ -121,5 +121,84 @@
"hindi": "हिन्दी", "hindi": "हिन्दी",
"indonesian": "Bahasa Indonesia", "indonesian": "Bahasa Indonesia",
"italian": "Italiano", "italian": "Italiano",
"arabic": "العربية" "arabic": "العربية",
"points": "Punkte",
"level": "Level",
"checked": "Geprüft",
"checkIn": "Einchecken",
"earnedPoints": "Verdient:",
"basePoints": "Basispunkte",
"honestyBonus": "Ehrlichkeitsbonus",
"totalPoints": "Gesamt Punkte: {count} ⚡",
"distractionsRecorded": "({count} {distractionText} aufgezeichnet)",
"achievementUnlocked": "🎖️ Erfolg freigeschaltet!",
"bonusPoints": "+{points} Punkte ⚡",
"checkInSuccess": "Check-in erfolgreich! +{points} Punkte ⚡",
"weeklyStreakBonus": "🎉 Wöchentlicher Streak-Bonus!",
"newAchievementUnlocked": "🎖️ Neuer Erfolg freigeschaltet!",
"alreadyCheckedIn": "Du hast heute bereits eingecheckt! Komm morgen wieder 📅",
"checkInCalendar": "Check-in-Kalender 📅",
"checkInToday": "📅 Heute einchecken",
"checkedInToday": "✓ Heute eingecheckt",
"currentStreak": "🔥 Aktueller Streak",
"longestStreak": "🏆 Längster Streak",
"days": "Tage",
"daysCount": "{count} Tage",
"achievements": "Erfolge 🎖️",
"viewAllAchievements": "Alle Erfolge anzeigen",
"allAchievementsComingSoon": "Vollständiger Erfolge-Bildschirm kommt bald!",
"profile": "Profil",
"focuser": "Fokussierer",
"pointsToNextLevel": "{points} Punkte bis Level {level}",
"achievement_first_session_name": "Fokus-Neuling",
"achievement_first_session_desc": "Schließe deine erste Fokussitzung ab",
"achievement_sessions_10_name": "Erste Schritte",
"achievement_sessions_10_desc": "Schließe 10 Fokussitzungen ab",
"achievement_sessions_50_name": "Fokus-Enthusiast",
"achievement_sessions_50_desc": "Schließe 50 Fokussitzungen ab",
"achievement_sessions_100_name": "Fokus-Meister",
"achievement_sessions_100_desc": "Schließe 100 Fokussitzungen ab",
"achievement_honest_bronze_name": "Ehrlicher Tracker · Bronze",
"achievement_honest_bronze_desc": "Zeichne 50 Ablenkungen ehrlich auf",
"achievement_honest_silver_name": "Ehrlicher Tracker · Silber",
"achievement_honest_silver_desc": "Zeichne 200 Ablenkungen ehrlich auf",
"achievement_honest_gold_name": "Ehrlicher Tracker · Gold",
"achievement_honest_gold_desc": "Zeichne 500 Ablenkungen ehrlich auf",
"achievement_marathon_name": "Marathon-Läufer",
"achievement_marathon_desc": "Sammle 10 Stunden Fokuszeit",
"achievement_century_name": "Jahrhundert-Club",
"achievement_century_desc": "Sammle 100 Stunden Fokuszeit",
"achievement_master_name": "Fokus-Großmeister",
"achievement_master_desc": "Sammle 1000 Stunden Fokuszeit",
"achievement_persistence_star_name": "Beharrlichkeitsstern",
"achievement_persistence_star_desc": "Checke 7 Tage in Folge ein",
"achievement_monthly_habit_name": "Monatliche Gewohnheit",
"achievement_monthly_habit_desc": "Checke 30 Tage in Folge ein",
"achievement_centurion_name": "Zenturio",
"achievement_centurion_desc": "Checke 100 Tage in Folge ein",
"achievement_year_warrior_name": "Jahreskrieger",
"achievement_year_warrior_desc": "Checke 365 Tage in Folge ein",
"total": "Gesamt",
"status": "Status",
"pointsBreakdown": "Punkteaufschlüsselung",
"focusTimePoints": "Fokuszeit",
"focusTimePointsDesc": "1 Punkt pro Minute Fokus",
"honestyBonusLabel": "Ehrlichkeitsbonus",
"honestyBonusDesc": "Extrapunkte für das Aufzeichnen von Ablenkungen",
"checkInPoints": "Täglicher Check-in",
"checkInPointsDesc": "Basispunkte für täglichen Check-in",
"streakBonus": "Streak-Bonus",
"streakBonusDesc": "{days} aufeinanderfolgende Check-ins",
"achievementBonusLabel": "Erfolgsbonus",
"weekdayS": "S",
"weekdayM": "M",
"weekdayT": "D",
"weekdayW": "M",
"weekdayTh": "D",
"weekdayF": "F",
"weekdaySa": "S"
} }

View File

@@ -231,5 +231,405 @@
"hindi": "हिन्दी (Hindi)", "hindi": "हिन्दी (Hindi)",
"indonesian": "Bahasa Indonesia (Indonesian)", "indonesian": "Bahasa Indonesia (Indonesian)",
"italian": "Italiano (Italian)", "italian": "Italiano (Italian)",
"arabic": "العربية (Arabic)" "arabic": "العربية (Arabic)",
"points": "Points",
"@points": {
"description": "Points label"
},
"level": "Level",
"@level": {
"description": "Level label"
},
"checked": "Checked",
"@checked": {
"description": "Already checked in today"
},
"checkIn": "Check In",
"@checkIn": {
"description": "Check in button text"
},
"earnedPoints": "Earned:",
"@earnedPoints": {
"description": "Points earned label on complete screen"
},
"basePoints": "Base Points",
"@basePoints": {
"description": "Base points from focus time"
},
"honestyBonus": "Honesty Bonus",
"@honestyBonus": {
"description": "Bonus points for recording distractions"
},
"totalPoints": "Total Points: {count} ⚡",
"@totalPoints": {
"description": "Total accumulated points",
"placeholders": {
"count": {
"type": "int"
}
}
},
"distractionsRecorded": "({count} {distractionText} recorded)",
"@distractionsRecorded": {
"description": "Number of distractions recorded",
"placeholders": {
"count": {
"type": "int"
},
"distractionText": {}
}
},
"achievementUnlocked": "🎖️ Achievement Unlocked!",
"@achievementUnlocked": {
"description": "Achievement unlocked title"
},
"bonusPoints": "+{points} Points ⚡",
"@bonusPoints": {
"description": "Bonus points awarded",
"placeholders": {
"points": {
"type": "int"
}
}
},
"checkInSuccess": "Check-in successful! +{points} points ⚡",
"@checkInSuccess": {
"description": "Check-in success message",
"placeholders": {
"points": {
"type": "int"
}
}
},
"weeklyStreakBonus": "🎉 Weekly streak bonus!",
"@weeklyStreakBonus": {
"description": "Weekly streak bonus message"
},
"newAchievementUnlocked": "🎖️ New achievement unlocked!",
"@newAchievementUnlocked": {
"description": "New achievement unlocked message"
},
"alreadyCheckedIn": "You have already checked in today! Come back tomorrow 📅",
"@alreadyCheckedIn": {
"description": "Already checked in message"
},
"checkInCalendar": "Check-In Calendar 📅",
"@checkInCalendar": {
"description": "Check-in calendar section title"
},
"checkInToday": "📅 Check In Today",
"@checkInToday": {
"description": "Check in today button"
},
"checkedInToday": "✓ Checked In Today",
"@checkedInToday": {
"description": "Already checked in today status"
},
"currentStreak": "🔥 Current Streak",
"@currentStreak": {
"description": "Current check-in streak label"
},
"longestStreak": "🏆 Longest Streak",
"@longestStreak": {
"description": "Longest check-in streak label"
},
"days": "days",
"@days": {
"description": "Days label"
},
"daysCount": "{count} days",
"@daysCount": {
"description": "Days with count",
"placeholders": {
"count": {
"type": "int"
}
}
},
"achievements": "Achievements 🎖️",
"@achievements": {
"description": "Achievements section title"
},
"viewAllAchievements": "View All Achievements",
"@viewAllAchievements": {
"description": "View all achievements button"
},
"allAchievementsComingSoon": "Full achievements screen coming soon!",
"@allAchievementsComingSoon": {
"description": "Coming soon message for full achievements screen"
},
"profile": "Profile",
"@profile": {
"description": "Profile screen title"
},
"focuser": "Focuser",
"@focuser": {
"description": "Default user name"
},
"pointsToNextLevel": "{points} points to Level {level}",
"@pointsToNextLevel": {
"description": "Points needed to reach next level",
"placeholders": {
"points": {
"type": "int"
},
"level": {
"type": "int"
}
}
},
"achievement_first_session_name": "Focus Newbie",
"@achievement_first_session_name": {
"description": "First session achievement name"
},
"achievement_first_session_desc": "Complete your first focus session",
"@achievement_first_session_desc": {
"description": "First session achievement description"
},
"achievement_sessions_10_name": "Getting Started",
"@achievement_sessions_10_name": {
"description": "10 sessions achievement name"
},
"achievement_sessions_10_desc": "Complete 10 focus sessions",
"@achievement_sessions_10_desc": {
"description": "10 sessions achievement description"
},
"achievement_sessions_50_name": "Focus Enthusiast",
"@achievement_sessions_50_name": {
"description": "50 sessions achievement name"
},
"achievement_sessions_50_desc": "Complete 50 focus sessions",
"@achievement_sessions_50_desc": {
"description": "50 sessions achievement description"
},
"achievement_sessions_100_name": "Focus Master",
"@achievement_sessions_100_name": {
"description": "100 sessions achievement name"
},
"achievement_sessions_100_desc": "Complete 100 focus sessions",
"@achievement_sessions_100_desc": {
"description": "100 sessions achievement description"
},
"achievement_honest_bronze_name": "Honest Tracker · Bronze",
"@achievement_honest_bronze_name": {
"description": "50 distractions achievement name"
},
"achievement_honest_bronze_desc": "Record 50 distractions honestly",
"@achievement_honest_bronze_desc": {
"description": "50 distractions achievement description"
},
"achievement_honest_silver_name": "Honest Tracker · Silver",
"@achievement_honest_silver_name": {
"description": "200 distractions achievement name"
},
"achievement_honest_silver_desc": "Record 200 distractions honestly",
"@achievement_honest_silver_desc": {
"description": "200 distractions achievement description"
},
"achievement_honest_gold_name": "Honest Tracker · Gold",
"@achievement_honest_gold_name": {
"description": "500 distractions achievement name"
},
"achievement_honest_gold_desc": "Record 500 distractions honestly",
"@achievement_honest_gold_desc": {
"description": "500 distractions achievement description"
},
"achievement_marathon_name": "Marathon Runner",
"@achievement_marathon_name": {
"description": "10 hours achievement name"
},
"achievement_marathon_desc": "Accumulate 10 hours of focus time",
"@achievement_marathon_desc": {
"description": "10 hours achievement description"
},
"achievement_century_name": "Century Club",
"@achievement_century_name": {
"description": "100 hours achievement name"
},
"achievement_century_desc": "Accumulate 100 hours of focus time",
"@achievement_century_desc": {
"description": "100 hours achievement description"
},
"achievement_master_name": "Focus Grandmaster",
"@achievement_master_name": {
"description": "1000 hours achievement name"
},
"achievement_master_desc": "Accumulate 1000 hours of focus time",
"@achievement_master_desc": {
"description": "1000 hours achievement description"
},
"achievement_persistence_star_name": "Persistence Star",
"@achievement_persistence_star_name": {
"description": "7 day streak achievement name"
},
"achievement_persistence_star_desc": "Check in for 7 consecutive days",
"@achievement_persistence_star_desc": {
"description": "7 day streak achievement description"
},
"achievement_monthly_habit_name": "Monthly Habit",
"@achievement_monthly_habit_name": {
"description": "30 day streak achievement name"
},
"achievement_monthly_habit_desc": "Check in for 30 consecutive days",
"@achievement_monthly_habit_desc": {
"description": "30 day streak achievement description"
},
"achievement_centurion_name": "Centurion",
"@achievement_centurion_name": {
"description": "100 day streak achievement name"
},
"achievement_centurion_desc": "Check in for 100 consecutive days",
"@achievement_centurion_desc": {
"description": "100 day streak achievement description"
},
"achievement_year_warrior_name": "Year Warrior",
"@achievement_year_warrior_name": {
"description": "365 day streak achievement name"
},
"achievement_year_warrior_desc": "Check in for 365 consecutive days",
"@achievement_year_warrior_desc": {
"description": "365 day streak achievement description"
},
"total": "Total",
"@total": {
"description": "Total label (e.g., total time)"
},
"status": "Status",
"@status": {
"description": "Status label"
},
"pointsBreakdown": "Points Breakdown",
"@pointsBreakdown": {
"description": "Points breakdown section title"
},
"focusTimePoints": "Focus Time",
"@focusTimePoints": {
"description": "Points from focus time label"
},
"focusTimePointsDesc": "1 point per minute of focus",
"@focusTimePointsDesc": {
"description": "Focus time points description"
},
"honestyBonusLabel": "Honesty Bonus",
"@honestyBonusLabel": {
"description": "Honesty bonus label in breakdown"
},
"honestyBonusDesc": "Extra points for recording distractions",
"@honestyBonusDesc": {
"description": "Honesty bonus description"
},
"checkInPoints": "Daily Check-In",
"@checkInPoints": {
"description": "Daily check-in points label"
},
"checkInPointsDesc": "Base points for daily check-in",
"@checkInPointsDesc": {
"description": "Daily check-in points description"
},
"streakBonus": "Streak Bonus",
"@streakBonus": {
"description": "Streak bonus label"
},
"streakBonusDesc": "{days} consecutive check-ins",
"@streakBonusDesc": {
"description": "Streak bonus description",
"placeholders": {
"days": {
"type": "int"
}
}
},
"achievementBonusLabel": "Achievement Bonus",
"@achievementBonusLabel": {
"description": "Achievement bonus points label"
},
"weekdayS": "S",
"@weekdayS": {
"description": "Sunday abbreviation"
},
"weekdayM": "M",
"@weekdayM": {
"description": "Monday abbreviation"
},
"weekdayT": "T",
"@weekdayT": {
"description": "Tuesday abbreviation"
},
"weekdayW": "W",
"@weekdayW": {
"description": "Wednesday abbreviation"
},
"weekdayTh": "T",
"@weekdayTh": {
"description": "Thursday abbreviation"
},
"weekdayF": "F",
"@weekdayF": {
"description": "Friday abbreviation"
},
"weekdaySa": "S",
"@weekdaySa": {
"description": "Saturday abbreviation"
}
} }

View File

@@ -121,5 +121,84 @@
"hindi": "हिन्दी", "hindi": "हिन्दी",
"indonesian": "Bahasa Indonesia", "indonesian": "Bahasa Indonesia",
"italian": "Italiano", "italian": "Italiano",
"arabic": "العربية" "arabic": "العربية",
"points": "Puntos",
"level": "Nivel",
"checked": "Registrado",
"checkIn": "Registrarse",
"earnedPoints": "Ganado:",
"basePoints": "Puntos Base",
"honestyBonus": "Bono de Honestidad",
"totalPoints": "Puntos Totales: {count} ⚡",
"distractionsRecorded": "({count} {distractionText} registradas)",
"achievementUnlocked": "🎖️ ¡Logro Desbloqueado!",
"bonusPoints": "+{points} Puntos ⚡",
"checkInSuccess": "¡Registro exitoso! +{points} puntos ⚡",
"weeklyStreakBonus": "🎉 ¡Bono de racha semanal!",
"newAchievementUnlocked": "🎖️ ¡Nuevo logro desbloqueado!",
"alreadyCheckedIn": "¡Ya te registraste hoy! Vuelve mañana 📅",
"checkInCalendar": "Calendario de Registro 📅",
"checkInToday": "📅 Registrarse Hoy",
"checkedInToday": "✓ Registrado Hoy",
"currentStreak": "🔥 Racha Actual",
"longestStreak": "🏆 Racha Más Larga",
"days": "días",
"daysCount": "{count} días",
"achievements": "Logros 🎖️",
"viewAllAchievements": "Ver Todos los Logros",
"allAchievementsComingSoon": "¡Pantalla completa de logros próximamente!",
"profile": "Perfil",
"focuser": "Enfocador",
"pointsToNextLevel": "{points} puntos para Nivel {level}",
"achievement_first_session_name": "Novato del Enfoque",
"achievement_first_session_desc": "Completa tu primera sesión de enfoque",
"achievement_sessions_10_name": "Comenzando",
"achievement_sessions_10_desc": "Completa 10 sesiones de enfoque",
"achievement_sessions_50_name": "Entusiasta del Enfoque",
"achievement_sessions_50_desc": "Completa 50 sesiones de enfoque",
"achievement_sessions_100_name": "Maestro del Enfoque",
"achievement_sessions_100_desc": "Completa 100 sesiones de enfoque",
"achievement_honest_bronze_name": "Registrador Honesto · Bronce",
"achievement_honest_bronze_desc": "Registra 50 distracciones honestamente",
"achievement_honest_silver_name": "Registrador Honesto · Plata",
"achievement_honest_silver_desc": "Registra 200 distracciones honestamente",
"achievement_honest_gold_name": "Registrador Honesto · Oro",
"achievement_honest_gold_desc": "Registra 500 distracciones honestamente",
"achievement_marathon_name": "Corredor de Maratón",
"achievement_marathon_desc": "Acumula 10 horas de tiempo de enfoque",
"achievement_century_name": "Club del Siglo",
"achievement_century_desc": "Acumula 100 horas de tiempo de enfoque",
"achievement_master_name": "Gran Maestro del Enfoque",
"achievement_master_desc": "Acumula 1000 horas de tiempo de enfoque",
"achievement_persistence_star_name": "Estrella de Persistencia",
"achievement_persistence_star_desc": "Regístrate durante 7 días consecutivos",
"achievement_monthly_habit_name": "Hábito Mensual",
"achievement_monthly_habit_desc": "Regístrate durante 30 días consecutivos",
"achievement_centurion_name": "Centurión",
"achievement_centurion_desc": "Regístrate durante 100 días consecutivos",
"achievement_year_warrior_name": "Guerrero del Año",
"achievement_year_warrior_desc": "Regístrate durante 365 días consecutivos",
"total": "Total",
"status": "Estado",
"pointsBreakdown": "Desglose de Puntos",
"focusTimePoints": "Tiempo de Enfoque",
"focusTimePointsDesc": "1 punto por minuto de enfoque",
"honestyBonusLabel": "Bono de Honestidad",
"honestyBonusDesc": "Puntos extra por registrar distracciones",
"checkInPoints": "Registro Diario",
"checkInPointsDesc": "Puntos base por primer registro del día",
"streakBonus": "Bono de Racha",
"streakBonusDesc": "{days} registros consecutivos",
"achievementBonusLabel": "Bono de Logro",
"weekdayS": "D",
"weekdayM": "L",
"weekdayT": "M",
"weekdayW": "X",
"weekdayTh": "J",
"weekdayF": "V",
"weekdaySa": "S"
} }

View File

@@ -121,5 +121,84 @@
"hindi": "हिन्दी", "hindi": "हिन्दी",
"indonesian": "Bahasa Indonesia", "indonesian": "Bahasa Indonesia",
"italian": "Italiano", "italian": "Italiano",
"arabic": "العربية" "arabic": "العربية",
"points": "Points",
"level": "Niveau",
"checked": "Vérifié",
"checkIn": "S'enregistrer",
"earnedPoints": "Gagné:",
"basePoints": "Points de base",
"honestyBonus": "Bonus d'honnêteté",
"totalPoints": "Total des points: {count} ⚡",
"distractionsRecorded": "({count} {distractionText} enregistrées)",
"achievementUnlocked": "🎖️ Succès débloqué!",
"bonusPoints": "+{points} Points ⚡",
"checkInSuccess": "Enregistrement réussi! +{points} points ⚡",
"weeklyStreakBonus": "🎉 Bonus de série hebdomadaire!",
"newAchievementUnlocked": "🎖️ Nouveau succès débloqué!",
"alreadyCheckedIn": "Vous vous êtes déjà enregistré aujourd'hui! Revenez demain 📅",
"checkInCalendar": "Calendrier d'enregistrement 📅",
"checkInToday": "📅 S'enregistrer aujourd'hui",
"checkedInToday": "✓ Enregistré aujourd'hui",
"currentStreak": "🔥 Série actuelle",
"longestStreak": "🏆 Plus longue série",
"days": "jours",
"daysCount": "{count} jours",
"achievements": "Succès 🎖️",
"viewAllAchievements": "Voir tous les succès",
"allAchievementsComingSoon": "Écran complet des succès bientôt disponible!",
"profile": "Profil",
"focuser": "Concentrateur",
"pointsToNextLevel": "{points} points jusqu'au niveau {level}",
"achievement_first_session_name": "Débutant en concentration",
"achievement_first_session_desc": "Complétez votre première session de concentration",
"achievement_sessions_10_name": "Premiers pas",
"achievement_sessions_10_desc": "Complétez 10 sessions de concentration",
"achievement_sessions_50_name": "Passionné de concentration",
"achievement_sessions_50_desc": "Complétez 50 sessions de concentration",
"achievement_sessions_100_name": "Maître de la concentration",
"achievement_sessions_100_desc": "Complétez 100 sessions de concentration",
"achievement_honest_bronze_name": "Tracker honnête · Bronze",
"achievement_honest_bronze_desc": "Enregistrez 50 distractions honnêtement",
"achievement_honest_silver_name": "Tracker honnête · Argent",
"achievement_honest_silver_desc": "Enregistrez 200 distractions honnêtement",
"achievement_honest_gold_name": "Tracker honnête · Or",
"achievement_honest_gold_desc": "Enregistrez 500 distractions honnêtement",
"achievement_marathon_name": "Coureur de marathon",
"achievement_marathon_desc": "Accumulez 10 heures de temps de concentration",
"achievement_century_name": "Club du siècle",
"achievement_century_desc": "Accumulez 100 heures de temps de concentration",
"achievement_master_name": "Grand maître de la concentration",
"achievement_master_desc": "Accumulez 1000 heures de temps de concentration",
"achievement_persistence_star_name": "Étoile de la persévérance",
"achievement_persistence_star_desc": "Enregistrez-vous pendant 7 jours consécutifs",
"achievement_monthly_habit_name": "Habitude mensuelle",
"achievement_monthly_habit_desc": "Enregistrez-vous pendant 30 jours consécutifs",
"achievement_centurion_name": "Centurion",
"achievement_centurion_desc": "Enregistrez-vous pendant 100 jours consécutifs",
"achievement_year_warrior_name": "Guerrier de l'année",
"achievement_year_warrior_desc": "Enregistrez-vous pendant 365 jours consécutifs",
"total": "Total",
"status": "Statut",
"pointsBreakdown": "Répartition des points",
"focusTimePoints": "Temps de concentration",
"focusTimePointsDesc": "1 point par minute de concentration",
"honestyBonusLabel": "Bonus d'honnêteté",
"honestyBonusDesc": "Points supplémentaires pour l'enregistrement des distractions",
"checkInPoints": "Enregistrement quotidien",
"checkInPointsDesc": "Points de base pour l'enregistrement quotidien",
"streakBonus": "Bonus de série",
"streakBonusDesc": "{days} enregistrements consécutifs",
"achievementBonusLabel": "Bonus de succès",
"weekdayS": "D",
"weekdayM": "L",
"weekdayT": "M",
"weekdayW": "M",
"weekdayTh": "J",
"weekdayF": "V",
"weekdaySa": "S"
} }

View File

@@ -121,5 +121,84 @@
"hindi": "हिन्दी", "hindi": "हिन्दी",
"indonesian": "Bahasa Indonesia", "indonesian": "Bahasa Indonesia",
"italian": "Italiano", "italian": "Italiano",
"arabic": "العربية" "arabic": "العربية",
"points": "अंक",
"level": "स्तर",
"checked": "चेक किया",
"checkIn": "चेक-इन",
"earnedPoints": "अर्जित:",
"basePoints": "मूल अंक",
"honestyBonus": "ईमानदारी बोनस",
"totalPoints": "कुल अंक: {count} ⚡",
"distractionsRecorded": "({count} {distractionText} रिकॉर्ड किया)",
"achievementUnlocked": "🎖️ उपलब्धि अनलॉक!",
"bonusPoints": "+{points} अंक ⚡",
"checkInSuccess": "चेक-इन सफल! +{points} अंक ⚡",
"weeklyStreakBonus": "🎉 साप्ताहिक स्ट्रीक बोनस!",
"newAchievementUnlocked": "🎖️ नई उपलब्धि अनलॉक!",
"alreadyCheckedIn": "आप आज पहले ही चेक-इन कर चुके हैं! कल वापस आएं 📅",
"checkInCalendar": "चेक-इन कैलेंडर 📅",
"checkInToday": "📅 आज चेक-इन करें",
"checkedInToday": "✓ आज चेक-इन हो गया",
"currentStreak": "🔥 वर्तमान स्ट्रीक",
"longestStreak": "🏆 सबसे लंबी स्ट्रीक",
"days": "दिन",
"daysCount": "{count} दिन",
"achievements": "उपलब्धियाँ 🎖️",
"viewAllAchievements": "सभी उपलब्धियाँ देखें",
"allAchievementsComingSoon": "पूर्ण उपलब्धि स्क्रीन जल्द आ रही है!",
"profile": "प्रोफ़ाइल",
"focuser": "फोकस करने वाला",
"pointsToNextLevel": "स्तर {level} के लिए {points} अंक",
"achievement_first_session_name": "फोकस नौसिखिया",
"achievement_first_session_desc": "अपना पहला फोकस सत्र पूरा करें",
"achievement_sessions_10_name": "शुरुआत",
"achievement_sessions_10_desc": "10 फोकस सत्र पूरे करें",
"achievement_sessions_50_name": "फोकस उत्साही",
"achievement_sessions_50_desc": "50 फोकस सत्र पूरे करें",
"achievement_sessions_100_name": "फोकस मास्टर",
"achievement_sessions_100_desc": "100 फोकस सत्र पूरे करें",
"achievement_honest_bronze_name": "ईमानदार ट्रैकर · कांस्य",
"achievement_honest_bronze_desc": "ईमानदारी से 50 विकर्षण रिकॉर्ड करें",
"achievement_honest_silver_name": "ईमानदार ट्रैकर · रजत",
"achievement_honest_silver_desc": "ईमानदारी से 200 विकर्षण रिकॉर्ड करें",
"achievement_honest_gold_name": "ईमानदार ट्रैकर · स्वर्ण",
"achievement_honest_gold_desc": "ईमानदारी से 500 विकर्षण रिकॉर्ड करें",
"achievement_marathon_name": "मैराथन धावक",
"achievement_marathon_desc": "10 घंटे का फोकस समय जमा करें",
"achievement_century_name": "सेंचुरी क्लब",
"achievement_century_desc": "100 घंटे का फोकस समय जमा करें",
"achievement_master_name": "फोकस ग्रैंडमास्टर",
"achievement_master_desc": "1000 घंटे का फोकस समय जमा करें",
"achievement_persistence_star_name": "दृढ़ता का सितारा",
"achievement_persistence_star_desc": "7 दिनों तक लगातार चेक-इन करें",
"achievement_monthly_habit_name": "मासिक आदत",
"achievement_monthly_habit_desc": "30 दिनों तक लगातार चेक-इन करें",
"achievement_centurion_name": "सेंचुरियन",
"achievement_centurion_desc": "100 दिनों तक लगातार चेक-इन करें",
"achievement_year_warrior_name": "वर्ष योद्धा",
"achievement_year_warrior_desc": "365 दिनों तक लगातार चेक-इन करें",
"total": "कुल",
"status": "स्थिति",
"pointsBreakdown": "अंकों का विवरण",
"focusTimePoints": "फोकस समय",
"focusTimePointsDesc": "फोकस के प्रति मिनट 1 अंक",
"honestyBonusLabel": "ईमानदारी बोनस",
"honestyBonusDesc": "विकर्षण रिकॉर्ड करने के लिए अतिरिक्त अंक",
"checkInPoints": "दैनिक चेक-इन",
"checkInPointsDesc": "दैनिक चेक-इन के लिए मूल अंक",
"streakBonus": "स्ट्रीक बोनस",
"streakBonusDesc": "{days} लगातार चेक-इन",
"achievementBonusLabel": "उपलब्धि बोनस",
"weekdayS": "र",
"weekdayM": "सो",
"weekdayT": "मं",
"weekdayW": "बु",
"weekdayTh": "गु",
"weekdayF": "शु",
"weekdaySa": "श"
} }

View File

@@ -121,5 +121,84 @@
"hindi": "हिन्दी", "hindi": "हिन्दी",
"indonesian": "Bahasa Indonesia", "indonesian": "Bahasa Indonesia",
"italian": "Italiano", "italian": "Italiano",
"arabic": "العربية" "arabic": "العربية",
"points": "Poin",
"level": "Level",
"checked": "Tercatat",
"checkIn": "Check-in",
"earnedPoints": "Diperoleh:",
"basePoints": "Poin Dasar",
"honestyBonus": "Bonus Kejujuran",
"totalPoints": "Total Poin: {count} ⚡",
"distractionsRecorded": "({count} {distractionText} tercatat)",
"achievementUnlocked": "🎖️ Pencapaian Terbuka!",
"bonusPoints": "+{points} Poin ⚡",
"checkInSuccess": "Check-in berhasil! +{points} poin ⚡",
"weeklyStreakBonus": "🎉 Bonus streak mingguan!",
"newAchievementUnlocked": "🎖️ Pencapaian baru terbuka!",
"alreadyCheckedIn": "Anda sudah check-in hari ini! Kembali lagi besok 📅",
"checkInCalendar": "Kalender Check-In 📅",
"checkInToday": "📅 Check-in Hari Ini",
"checkedInToday": "✓ Sudah Check-in Hari Ini",
"currentStreak": "🔥 Streak Saat Ini",
"longestStreak": "🏆 Streak Terpanjang",
"days": "hari",
"daysCount": "{count} hari",
"achievements": "Pencapaian 🎖️",
"viewAllAchievements": "Lihat Semua Pencapaian",
"allAchievementsComingSoon": "Layar pencapaian lengkap segera hadir!",
"profile": "Profil",
"focuser": "Pemfokus",
"pointsToNextLevel": "{points} poin menuju Level {level}",
"achievement_first_session_name": "Pemula Fokus",
"achievement_first_session_desc": "Selesaikan sesi fokus pertama Anda",
"achievement_sessions_10_name": "Memulai",
"achievement_sessions_10_desc": "Selesaikan 10 sesi fokus",
"achievement_sessions_50_name": "Penggemar Fokus",
"achievement_sessions_50_desc": "Selesaikan 50 sesi fokus",
"achievement_sessions_100_name": "Master Fokus",
"achievement_sessions_100_desc": "Selesaikan 100 sesi fokus",
"achievement_honest_bronze_name": "Pelacak Jujur · Perunggu",
"achievement_honest_bronze_desc": "Catat 50 gangguan dengan jujur",
"achievement_honest_silver_name": "Pelacak Jujur · Perak",
"achievement_honest_silver_desc": "Catat 200 gangguan dengan jujur",
"achievement_honest_gold_name": "Pelacak Jujur · Emas",
"achievement_honest_gold_desc": "Catat 500 gangguan dengan jujur",
"achievement_marathon_name": "Pelari Maraton",
"achievement_marathon_desc": "Kumpulkan 10 jam waktu fokus",
"achievement_century_name": "Klub Abad",
"achievement_century_desc": "Kumpulkan 100 jam waktu fokus",
"achievement_master_name": "Grandmaster Fokus",
"achievement_master_desc": "Kumpulkan 1000 jam waktu fokus",
"achievement_persistence_star_name": "Bintang Kegigihan",
"achievement_persistence_star_desc": "Check-in selama 7 hari berturut-turut",
"achievement_monthly_habit_name": "Kebiasaan Bulanan",
"achievement_monthly_habit_desc": "Check-in selama 30 hari berturut-turut",
"achievement_centurion_name": "Centurion",
"achievement_centurion_desc": "Check-in selama 100 hari berturut-turut",
"achievement_year_warrior_name": "Pejuang Tahun",
"achievement_year_warrior_desc": "Check-in selama 365 hari berturut-turut",
"total": "Total",
"status": "Status",
"pointsBreakdown": "Rincian Poin",
"focusTimePoints": "Waktu Fokus",
"focusTimePointsDesc": "1 poin per menit fokus",
"honestyBonusLabel": "Bonus Kejujuran",
"honestyBonusDesc": "Poin tambahan untuk mencatat gangguan",
"checkInPoints": "Check-in Harian",
"checkInPointsDesc": "Poin dasar untuk check-in harian",
"streakBonus": "Bonus Streak",
"streakBonusDesc": "{days} check-in berturut-turut",
"achievementBonusLabel": "Bonus Pencapaian",
"weekdayS": "M",
"weekdayM": "S",
"weekdayT": "S",
"weekdayW": "R",
"weekdayTh": "K",
"weekdayF": "J",
"weekdaySa": "S"
} }

View File

@@ -121,5 +121,84 @@
"hindi": "हिन्दी", "hindi": "हिन्दी",
"indonesian": "Bahasa Indonesia", "indonesian": "Bahasa Indonesia",
"italian": "Italiano", "italian": "Italiano",
"arabic": "العربية" "arabic": "العربية",
"points": "Punti",
"level": "Livello",
"checked": "Registrato",
"checkIn": "Check-in",
"earnedPoints": "Guadagnato:",
"basePoints": "Punti Base",
"honestyBonus": "Bonus Onestà",
"totalPoints": "Punti Totali: {count} ⚡",
"distractionsRecorded": "({count} {distractionText} registrate)",
"achievementUnlocked": "🎖️ Obiettivo Sbloccato!",
"bonusPoints": "+{points} Punti ⚡",
"checkInSuccess": "Check-in riuscito! +{points} punti ⚡",
"weeklyStreakBonus": "🎉 Bonus serie settimanale!",
"newAchievementUnlocked": "🎖️ Nuovo obiettivo sbloccato!",
"alreadyCheckedIn": "Hai già fatto il check-in oggi! Torna domani 📅",
"checkInCalendar": "Calendario Check-In 📅",
"checkInToday": "📅 Check-in Oggi",
"checkedInToday": "✓ Check-in Fatto Oggi",
"currentStreak": "🔥 Serie Attuale",
"longestStreak": "🏆 Serie Più Lunga",
"days": "giorni",
"daysCount": "{count} giorni",
"achievements": "Obiettivi 🎖️",
"viewAllAchievements": "Vedi Tutti gli Obiettivi",
"allAchievementsComingSoon": "Schermata completa degli obiettivi in arrivo!",
"profile": "Profilo",
"focuser": "Concentratore",
"pointsToNextLevel": "{points} punti al Livello {level}",
"achievement_first_session_name": "Principiante della Concentrazione",
"achievement_first_session_desc": "Completa la tua prima sessione di concentrazione",
"achievement_sessions_10_name": "Inizio",
"achievement_sessions_10_desc": "Completa 10 sessioni di concentrazione",
"achievement_sessions_50_name": "Appassionato di Concentrazione",
"achievement_sessions_50_desc": "Completa 50 sessioni di concentrazione",
"achievement_sessions_100_name": "Maestro della Concentrazione",
"achievement_sessions_100_desc": "Completa 100 sessioni di concentrazione",
"achievement_honest_bronze_name": "Tracker Onesto · Bronzo",
"achievement_honest_bronze_desc": "Registra onestamente 50 distrazioni",
"achievement_honest_silver_name": "Tracker Onesto · Argento",
"achievement_honest_silver_desc": "Registra onestamente 200 distrazioni",
"achievement_honest_gold_name": "Tracker Onesto · Oro",
"achievement_honest_gold_desc": "Registra onestamente 500 distrazioni",
"achievement_marathon_name": "Maratoneta",
"achievement_marathon_desc": "Accumula 10 ore di tempo di concentrazione",
"achievement_century_name": "Club del Secolo",
"achievement_century_desc": "Accumula 100 ore di tempo di concentrazione",
"achievement_master_name": "Gran Maestro della Concentrazione",
"achievement_master_desc": "Accumula 1000 ore di tempo di concentrazione",
"achievement_persistence_star_name": "Stella della Persistenza",
"achievement_persistence_star_desc": "Fai il check-in per 7 giorni consecutivi",
"achievement_monthly_habit_name": "Abitudine Mensile",
"achievement_monthly_habit_desc": "Fai il check-in per 30 giorni consecutivi",
"achievement_centurion_name": "Centurione",
"achievement_centurion_desc": "Fai il check-in per 100 giorni consecutivi",
"achievement_year_warrior_name": "Guerriero dell'Anno",
"achievement_year_warrior_desc": "Fai il check-in per 365 giorni consecutivi",
"total": "Totale",
"status": "Stato",
"pointsBreakdown": "Dettaglio Punti",
"focusTimePoints": "Tempo di Concentrazione",
"focusTimePointsDesc": "1 punto per minuto di concentrazione",
"honestyBonusLabel": "Bonus Onestà",
"honestyBonusDesc": "Punti extra per registrare distrazioni",
"checkInPoints": "Check-in Giornaliero",
"checkInPointsDesc": "Punti base per check-in giornaliero",
"streakBonus": "Bonus Serie",
"streakBonusDesc": "{days} check-in consecutivi",
"achievementBonusLabel": "Bonus Obiettivo",
"weekdayS": "D",
"weekdayM": "L",
"weekdayT": "M",
"weekdayW": "M",
"weekdayTh": "G",
"weekdayF": "V",
"weekdaySa": "S"
} }

View File

@@ -121,5 +121,84 @@
"hindi": "हिन्दी", "hindi": "हिन्दी",
"indonesian": "Bahasa Indonesia", "indonesian": "Bahasa Indonesia",
"italian": "Italiano", "italian": "Italiano",
"arabic": "العربية" "arabic": "العربية",
"points": "ポイント",
"level": "レベル",
"checked": "チェック済み",
"checkIn": "チェックイン",
"earnedPoints": "獲得:",
"basePoints": "基本ポイント",
"honestyBonus": "正直ボーナス",
"totalPoints": "合計ポイント:{count} ⚡",
"distractionsRecorded": "({count} {distractionText} 記録済み)",
"achievementUnlocked": "🎖️ 実績解除!",
"bonusPoints": "+{points} ポイント ⚡",
"checkInSuccess": "チェックイン成功!+{points} ポイント ⚡",
"weeklyStreakBonus": "🎉 1週間連続ボーナス",
"newAchievementUnlocked": "🎖️ 新しい実績解除!",
"alreadyCheckedIn": "今日は既にチェックイン済みです!明日また来てください 📅",
"checkInCalendar": "チェックインカレンダー 📅",
"checkInToday": "📅 今日チェックイン",
"checkedInToday": "✓ 今日チェックイン済み",
"currentStreak": "🔥 現在の連続",
"longestStreak": "🏆 最長連続",
"days": "日",
"daysCount": "{count} 日",
"achievements": "実績 🎖️",
"viewAllAchievements": "すべての実績を見る",
"allAchievementsComingSoon": "完全な実績画面は近日公開!",
"profile": "プロフィール",
"focuser": "集中する人",
"pointsToNextLevel": "レベル {level} まであと {points} ポイント",
"achievement_first_session_name": "集中初心者",
"achievement_first_session_desc": "最初の集中セッションを完了",
"achievement_sessions_10_name": "入門者",
"achievement_sessions_10_desc": "10回の集中セッションを完了",
"achievement_sessions_50_name": "集中愛好家",
"achievement_sessions_50_desc": "50回の集中セッションを完了",
"achievement_sessions_100_name": "集中マスター",
"achievement_sessions_100_desc": "100回の集中セッションを完了",
"achievement_honest_bronze_name": "正直な記録者・ブロンズ",
"achievement_honest_bronze_desc": "50回の気の散りを正直に記録",
"achievement_honest_silver_name": "正直な記録者・シルバー",
"achievement_honest_silver_desc": "200回の気の散りを正直に記録",
"achievement_honest_gold_name": "正直な記録者・ゴールド",
"achievement_honest_gold_desc": "500回の気の散りを正直に記録",
"achievement_marathon_name": "マラソンランナー",
"achievement_marathon_desc": "10時間の集中時間を累積",
"achievement_century_name": "センチュリークラブ",
"achievement_century_desc": "100時間の集中時間を累積",
"achievement_master_name": "集中グランドマスター",
"achievement_master_desc": "1000時間の集中時間を累積",
"achievement_persistence_star_name": "継続の星",
"achievement_persistence_star_desc": "7日間連続でチェックイン",
"achievement_monthly_habit_name": "月間習慣",
"achievement_monthly_habit_desc": "30日間連続でチェックイン",
"achievement_centurion_name": "百日戦士",
"achievement_centurion_desc": "100日間連続でチェックイン",
"achievement_year_warrior_name": "年間戦士",
"achievement_year_warrior_desc": "365日間連続でチェックイン",
"total": "合計",
"status": "ステータス",
"pointsBreakdown": "ポイント内訳",
"focusTimePoints": "集中時間",
"focusTimePointsDesc": "1分の集中につき1ポイント",
"honestyBonusLabel": "正直ボーナス",
"honestyBonusDesc": "気の散りを記録すると追加ポイント",
"checkInPoints": "毎日チェックイン",
"checkInPointsDesc": "毎日の初回チェックインで基本ポイント",
"streakBonus": "連続ボーナス",
"streakBonusDesc": "{days} 日連続チェックイン",
"achievementBonusLabel": "実績ボーナス",
"weekdayS": "日",
"weekdayM": "月",
"weekdayT": "火",
"weekdayW": "水",
"weekdayTh": "木",
"weekdayF": "金",
"weekdaySa": "土"
} }

View File

@@ -121,5 +121,84 @@
"hindi": "हिन्दी", "hindi": "हिन्दी",
"indonesian": "Bahasa Indonesia", "indonesian": "Bahasa Indonesia",
"italian": "Italiano", "italian": "Italiano",
"arabic": "العربية" "arabic": "العربية",
"points": "포인트",
"level": "레벨",
"checked": "체크 완료",
"checkIn": "체크인",
"earnedPoints": "획득:",
"basePoints": "기본 포인트",
"honestyBonus": "정직 보너스",
"totalPoints": "총 포인트: {count} ⚡",
"distractionsRecorded": "({count} {distractionText} 기록됨)",
"achievementUnlocked": "🎖️ 업적 달성!",
"bonusPoints": "+{points} 포인트 ⚡",
"checkInSuccess": "체크인 성공! +{points} 포인트 ⚡",
"weeklyStreakBonus": "🎉 주간 연속 보너스!",
"newAchievementUnlocked": "🎖️ 새로운 업적 달성!",
"alreadyCheckedIn": "오늘 이미 체크인했어요! 내일 다시 오세요 📅",
"checkInCalendar": "체크인 캘린더 📅",
"checkInToday": "📅 오늘 체크인",
"checkedInToday": "✓ 오늘 체크인 완료",
"currentStreak": "🔥 현재 연속",
"longestStreak": "🏆 최장 연속",
"days": "일",
"daysCount": "{count} 일",
"achievements": "업적 🎖️",
"viewAllAchievements": "모든 업적 보기",
"allAchievementsComingSoon": "전체 업적 화면 곧 공개!",
"profile": "프로필",
"focuser": "집중하는 사람",
"pointsToNextLevel": "레벨 {level}까지 {points} 포인트 남음",
"achievement_first_session_name": "집중 초보자",
"achievement_first_session_desc": "첫 집중 세션 완료",
"achievement_sessions_10_name": "시작 단계",
"achievement_sessions_10_desc": "10회 집중 세션 완료",
"achievement_sessions_50_name": "집중 애호가",
"achievement_sessions_50_desc": "50회 집중 세션 완료",
"achievement_sessions_100_name": "집중 마스터",
"achievement_sessions_100_desc": "100회 집중 세션 완료",
"achievement_honest_bronze_name": "정직한 기록자 · 브론즈",
"achievement_honest_bronze_desc": "50회 산만함을 정직하게 기록",
"achievement_honest_silver_name": "정직한 기록자 · 실버",
"achievement_honest_silver_desc": "200회 산만함을 정직하게 기록",
"achievement_honest_gold_name": "정직한 기록자 · 골드",
"achievement_honest_gold_desc": "500회 산만함을 정직하게 기록",
"achievement_marathon_name": "마라톤 러너",
"achievement_marathon_desc": "누적 10시간 집중",
"achievement_century_name": "센추리 클럽",
"achievement_century_desc": "누적 100시간 집중",
"achievement_master_name": "집중 그랜드마스터",
"achievement_master_desc": "누적 1000시간 집중",
"achievement_persistence_star_name": "끈기의 별",
"achievement_persistence_star_desc": "7일 연속 체크인",
"achievement_monthly_habit_name": "월간 습관",
"achievement_monthly_habit_desc": "30일 연속 체크인",
"achievement_centurion_name": "백일 전사",
"achievement_centurion_desc": "100일 연속 체크인",
"achievement_year_warrior_name": "연간 전사",
"achievement_year_warrior_desc": "365일 연속 체크인",
"total": "합계",
"status": "상태",
"pointsBreakdown": "포인트 세부 내역",
"focusTimePoints": "집중 시간",
"focusTimePointsDesc": "1분 집중당 1포인트",
"honestyBonusLabel": "정직 보너스",
"honestyBonusDesc": "산만함 기록 시 추가 포인트",
"checkInPoints": "일일 체크인",
"checkInPointsDesc": "매일 첫 체크인 시 기본 포인트",
"streakBonus": "연속 보너스",
"streakBonusDesc": "{days}일 연속 체크인",
"achievementBonusLabel": "업적 보너스",
"weekdayS": "일",
"weekdayM": "월",
"weekdayT": "화",
"weekdayW": "수",
"weekdayTh": "목",
"weekdayF": "금",
"weekdaySa": "토"
} }

View File

@@ -656,6 +656,456 @@ abstract class AppLocalizations {
/// In en, this message translates to: /// In en, this message translates to:
/// **'العربية (Arabic)'** /// **'العربية (Arabic)'**
String get arabic; String get arabic;
/// Points label
///
/// In en, this message translates to:
/// **'Points'**
String get points;
/// Level label
///
/// In en, this message translates to:
/// **'Level'**
String get level;
/// Already checked in today
///
/// In en, this message translates to:
/// **'Checked'**
String get checked;
/// Check in button text
///
/// In en, this message translates to:
/// **'Check In'**
String get checkIn;
/// Points earned label on complete screen
///
/// In en, this message translates to:
/// **'Earned:'**
String get earnedPoints;
/// Base points from focus time
///
/// In en, this message translates to:
/// **'Base Points'**
String get basePoints;
/// Bonus points for recording distractions
///
/// In en, this message translates to:
/// **'Honesty Bonus'**
String get honestyBonus;
/// Total accumulated points
///
/// In en, this message translates to:
/// **'Total Points: {count} ⚡'**
String totalPoints(int count);
/// Number of distractions recorded
///
/// In en, this message translates to:
/// **'({count} {distractionText} recorded)'**
String distractionsRecorded(int count, Object distractionText);
/// Achievement unlocked title
///
/// In en, this message translates to:
/// **'🎖️ Achievement Unlocked!'**
String get achievementUnlocked;
/// Bonus points awarded
///
/// In en, this message translates to:
/// **'+{points} Points ⚡'**
String bonusPoints(int points);
/// Check-in success message
///
/// In en, this message translates to:
/// **'Check-in successful! +{points} points ⚡'**
String checkInSuccess(int points);
/// Weekly streak bonus message
///
/// In en, this message translates to:
/// **'🎉 Weekly streak bonus!'**
String get weeklyStreakBonus;
/// New achievement unlocked message
///
/// In en, this message translates to:
/// **'🎖️ New achievement unlocked!'**
String get newAchievementUnlocked;
/// Already checked in message
///
/// In en, this message translates to:
/// **'You have already checked in today! Come back tomorrow 📅'**
String get alreadyCheckedIn;
/// Check-in calendar section title
///
/// In en, this message translates to:
/// **'Check-In Calendar 📅'**
String get checkInCalendar;
/// Check in today button
///
/// In en, this message translates to:
/// **'📅 Check In Today'**
String get checkInToday;
/// Already checked in today status
///
/// In en, this message translates to:
/// **'✓ Checked In Today'**
String get checkedInToday;
/// Current check-in streak label
///
/// In en, this message translates to:
/// **'🔥 Current Streak'**
String get currentStreak;
/// Longest check-in streak label
///
/// In en, this message translates to:
/// **'🏆 Longest Streak'**
String get longestStreak;
/// Days label
///
/// In en, this message translates to:
/// **'days'**
String get days;
/// Days with count
///
/// In en, this message translates to:
/// **'{count} days'**
String daysCount(int count);
/// Achievements section title
///
/// In en, this message translates to:
/// **'Achievements 🎖️'**
String get achievements;
/// View all achievements button
///
/// In en, this message translates to:
/// **'View All Achievements'**
String get viewAllAchievements;
/// Coming soon message for full achievements screen
///
/// In en, this message translates to:
/// **'Full achievements screen coming soon!'**
String get allAchievementsComingSoon;
/// Profile screen title
///
/// In en, this message translates to:
/// **'Profile'**
String get profile;
/// Default user name
///
/// In en, this message translates to:
/// **'Focuser'**
String get focuser;
/// Points needed to reach next level
///
/// In en, this message translates to:
/// **'{points} points to Level {level}'**
String pointsToNextLevel(int points, int level);
/// First session achievement name
///
/// In en, this message translates to:
/// **'Focus Newbie'**
String get achievement_first_session_name;
/// First session achievement description
///
/// In en, this message translates to:
/// **'Complete your first focus session'**
String get achievement_first_session_desc;
/// 10 sessions achievement name
///
/// In en, this message translates to:
/// **'Getting Started'**
String get achievement_sessions_10_name;
/// 10 sessions achievement description
///
/// In en, this message translates to:
/// **'Complete 10 focus sessions'**
String get achievement_sessions_10_desc;
/// 50 sessions achievement name
///
/// In en, this message translates to:
/// **'Focus Enthusiast'**
String get achievement_sessions_50_name;
/// 50 sessions achievement description
///
/// In en, this message translates to:
/// **'Complete 50 focus sessions'**
String get achievement_sessions_50_desc;
/// 100 sessions achievement name
///
/// In en, this message translates to:
/// **'Focus Master'**
String get achievement_sessions_100_name;
/// 100 sessions achievement description
///
/// In en, this message translates to:
/// **'Complete 100 focus sessions'**
String get achievement_sessions_100_desc;
/// 50 distractions achievement name
///
/// In en, this message translates to:
/// **'Honest Tracker · Bronze'**
String get achievement_honest_bronze_name;
/// 50 distractions achievement description
///
/// In en, this message translates to:
/// **'Record 50 distractions honestly'**
String get achievement_honest_bronze_desc;
/// 200 distractions achievement name
///
/// In en, this message translates to:
/// **'Honest Tracker · Silver'**
String get achievement_honest_silver_name;
/// 200 distractions achievement description
///
/// In en, this message translates to:
/// **'Record 200 distractions honestly'**
String get achievement_honest_silver_desc;
/// 500 distractions achievement name
///
/// In en, this message translates to:
/// **'Honest Tracker · Gold'**
String get achievement_honest_gold_name;
/// 500 distractions achievement description
///
/// In en, this message translates to:
/// **'Record 500 distractions honestly'**
String get achievement_honest_gold_desc;
/// 10 hours achievement name
///
/// In en, this message translates to:
/// **'Marathon Runner'**
String get achievement_marathon_name;
/// 10 hours achievement description
///
/// In en, this message translates to:
/// **'Accumulate 10 hours of focus time'**
String get achievement_marathon_desc;
/// 100 hours achievement name
///
/// In en, this message translates to:
/// **'Century Club'**
String get achievement_century_name;
/// 100 hours achievement description
///
/// In en, this message translates to:
/// **'Accumulate 100 hours of focus time'**
String get achievement_century_desc;
/// 1000 hours achievement name
///
/// In en, this message translates to:
/// **'Focus Grandmaster'**
String get achievement_master_name;
/// 1000 hours achievement description
///
/// In en, this message translates to:
/// **'Accumulate 1000 hours of focus time'**
String get achievement_master_desc;
/// 7 day streak achievement name
///
/// In en, this message translates to:
/// **'Persistence Star'**
String get achievement_persistence_star_name;
/// 7 day streak achievement description
///
/// In en, this message translates to:
/// **'Check in for 7 consecutive days'**
String get achievement_persistence_star_desc;
/// 30 day streak achievement name
///
/// In en, this message translates to:
/// **'Monthly Habit'**
String get achievement_monthly_habit_name;
/// 30 day streak achievement description
///
/// In en, this message translates to:
/// **'Check in for 30 consecutive days'**
String get achievement_monthly_habit_desc;
/// 100 day streak achievement name
///
/// In en, this message translates to:
/// **'Centurion'**
String get achievement_centurion_name;
/// 100 day streak achievement description
///
/// In en, this message translates to:
/// **'Check in for 100 consecutive days'**
String get achievement_centurion_desc;
/// 365 day streak achievement name
///
/// In en, this message translates to:
/// **'Year Warrior'**
String get achievement_year_warrior_name;
/// 365 day streak achievement description
///
/// In en, this message translates to:
/// **'Check in for 365 consecutive days'**
String get achievement_year_warrior_desc;
/// Total label (e.g., total time)
///
/// In en, this message translates to:
/// **'Total'**
String get total;
/// Status label
///
/// In en, this message translates to:
/// **'Status'**
String get status;
/// Points breakdown section title
///
/// In en, this message translates to:
/// **'Points Breakdown'**
String get pointsBreakdown;
/// Points from focus time label
///
/// In en, this message translates to:
/// **'Focus Time'**
String get focusTimePoints;
/// Focus time points description
///
/// In en, this message translates to:
/// **'1 point per minute of focus'**
String get focusTimePointsDesc;
/// Honesty bonus label in breakdown
///
/// In en, this message translates to:
/// **'Honesty Bonus'**
String get honestyBonusLabel;
/// Honesty bonus description
///
/// In en, this message translates to:
/// **'Extra points for recording distractions'**
String get honestyBonusDesc;
/// Daily check-in points label
///
/// In en, this message translates to:
/// **'Daily Check-In'**
String get checkInPoints;
/// Daily check-in points description
///
/// In en, this message translates to:
/// **'Base points for daily check-in'**
String get checkInPointsDesc;
/// Streak bonus label
///
/// In en, this message translates to:
/// **'Streak Bonus'**
String get streakBonus;
/// Streak bonus description
///
/// In en, this message translates to:
/// **'{days} consecutive check-ins'**
String streakBonusDesc(int days);
/// Achievement bonus points label
///
/// In en, this message translates to:
/// **'Achievement Bonus'**
String get achievementBonusLabel;
/// Sunday abbreviation
///
/// In en, this message translates to:
/// **'S'**
String get weekdayS;
/// Monday abbreviation
///
/// In en, this message translates to:
/// **'M'**
String get weekdayM;
/// Tuesday abbreviation
///
/// In en, this message translates to:
/// **'T'**
String get weekdayT;
/// Wednesday abbreviation
///
/// In en, this message translates to:
/// **'W'**
String get weekdayW;
/// Thursday abbreviation
///
/// In en, this message translates to:
/// **'T'**
String get weekdayTh;
/// Friday abbreviation
///
/// In en, this message translates to:
/// **'F'**
String get weekdayF;
/// Saturday abbreviation
///
/// In en, this message translates to:
/// **'S'**
String get weekdaySa;
} }
class _AppLocalizationsDelegate class _AppLocalizationsDelegate

View File

@@ -333,4 +333,246 @@ class AppLocalizationsAr extends AppLocalizations {
@override @override
String get arabic => 'العربية'; String get arabic => 'العربية';
@override
String get points => 'النقاط';
@override
String get level => 'المستوى';
@override
String get checked => 'تم التسجيل';
@override
String get checkIn => 'تسجيل الحضور';
@override
String get earnedPoints => 'المكتسب:';
@override
String get basePoints => 'النقاط الأساسية';
@override
String get honestyBonus => 'مكافأة الصدق';
@override
String totalPoints(int count) {
return 'إجمالي النقاط: $count';
}
@override
String distractionsRecorded(int count, Object distractionText) {
return '($count $distractionText مسجلة)';
}
@override
String get achievementUnlocked => '🎖️ إنجاز مفتوح!';
@override
String bonusPoints(int points) {
return '+$points نقاط ⚡';
}
@override
String checkInSuccess(int points) {
return 'تسجيل الحضور ناجح! +$points نقاط ⚡';
}
@override
String get weeklyStreakBonus => '🎉 مكافأة السلسلة الأسبوعية!';
@override
String get newAchievementUnlocked => '🎖️ إنجاز جديد مفتوح!';
@override
String get alreadyCheckedIn => 'لقد سجلت حضورك اليوم بالفعل! عد غدًا 📅';
@override
String get checkInCalendar => 'تقويم تسجيل الحضور 📅';
@override
String get checkInToday => '📅 سجل الحضور اليوم';
@override
String get checkedInToday => '✓ تم التسجيل اليوم';
@override
String get currentStreak => '🔥 السلسلة الحالية';
@override
String get longestStreak => '🏆 أطول سلسلة';
@override
String get days => 'أيام';
@override
String daysCount(int count) {
return '$count أيام';
}
@override
String get achievements => 'الإنجازات 🎖️';
@override
String get viewAllAchievements => 'عرض جميع الإنجازات';
@override
String get allAchievementsComingSoon => 'شاشة الإنجازات الكاملة قريبًا!';
@override
String get profile => 'الملف الشخصي';
@override
String get focuser => 'المركز';
@override
String pointsToNextLevel(int points, int level) {
return '$points نقاط إلى المستوى $level';
}
@override
String get achievement_first_session_name => 'مبتدئ التركيز';
@override
String get achievement_first_session_desc => 'أكمل جلسة التركيز الأولى';
@override
String get achievement_sessions_10_name => 'البداية';
@override
String get achievement_sessions_10_desc => 'أكمل 10 جلسات تركيز';
@override
String get achievement_sessions_50_name => 'عاشق التركيز';
@override
String get achievement_sessions_50_desc => 'أكمل 50 جلسة تركيز';
@override
String get achievement_sessions_100_name => 'سيد التركيز';
@override
String get achievement_sessions_100_desc => 'أكمل 100 جلسة تركيز';
@override
String get achievement_honest_bronze_name => 'المتتبع الصادق · برونزي';
@override
String get achievement_honest_bronze_desc => 'سجل 50 تشتتًا بصدق';
@override
String get achievement_honest_silver_name => 'المتتبع الصادق · فضي';
@override
String get achievement_honest_silver_desc => 'سجل 200 تشتت بصدق';
@override
String get achievement_honest_gold_name => 'المتتبع الصادق · ذهبي';
@override
String get achievement_honest_gold_desc => 'سجل 500 تشتت بصدق';
@override
String get achievement_marathon_name => 'عداء الماراثون';
@override
String get achievement_marathon_desc => 'اجمع 10 ساعات من وقت التركيز';
@override
String get achievement_century_name => 'نادي القرن';
@override
String get achievement_century_desc => 'اجمع 100 ساعة من وقت التركيز';
@override
String get achievement_master_name => 'جراند ماستر التركيز';
@override
String get achievement_master_desc => 'اجمع 1000 ساعة من وقت التركيز';
@override
String get achievement_persistence_star_name => 'نجمة المثابرة';
@override
String get achievement_persistence_star_desc =>
'سجل الحضور لمدة 7 أيام متتالية';
@override
String get achievement_monthly_habit_name => 'العادة الشهرية';
@override
String get achievement_monthly_habit_desc =>
'سجل الحضور لمدة 30 يومًا متتاليًا';
@override
String get achievement_centurion_name => 'المئوي';
@override
String get achievement_centurion_desc => 'سجل الحضور لمدة 100 يوم متتالٍ';
@override
String get achievement_year_warrior_name => 'محارب العام';
@override
String get achievement_year_warrior_desc =>
'سجل الحضور لمدة 365 يومًا متتاليًا';
@override
String get total => 'الإجمالي';
@override
String get status => 'الحالة';
@override
String get pointsBreakdown => 'تفصيل النقاط';
@override
String get focusTimePoints => 'وقت التركيز';
@override
String get focusTimePointsDesc => 'نقطة واحدة لكل دقيقة تركيز';
@override
String get honestyBonusLabel => 'مكافأة الصدق';
@override
String get honestyBonusDesc => 'نقاط إضافية لتسجيل التشتتات';
@override
String get checkInPoints => 'تسجيل الحضور اليومي';
@override
String get checkInPointsDesc => 'النقاط الأساسية لتسجيل الحضور اليومي';
@override
String get streakBonus => 'مكافأة السلسلة';
@override
String streakBonusDesc(int days) {
return '$days تسجيلات حضور متتالية';
}
@override
String get achievementBonusLabel => 'مكافأة الإنجاز';
@override
String get weekdayS => 'ح';
@override
String get weekdayM => 'ن';
@override
String get weekdayT => 'ث';
@override
String get weekdayW => 'ر';
@override
String get weekdayTh => 'خ';
@override
String get weekdayF => 'ج';
@override
String get weekdaySa => 'س';
} }

View File

@@ -336,4 +336,250 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get arabic => 'العربية'; String get arabic => 'العربية';
@override
String get points => 'Punkte';
@override
String get level => 'Level';
@override
String get checked => 'Geprüft';
@override
String get checkIn => 'Einchecken';
@override
String get earnedPoints => 'Verdient:';
@override
String get basePoints => 'Basispunkte';
@override
String get honestyBonus => 'Ehrlichkeitsbonus';
@override
String totalPoints(int count) {
return 'Gesamt Punkte: $count';
}
@override
String distractionsRecorded(int count, Object distractionText) {
return '($count $distractionText aufgezeichnet)';
}
@override
String get achievementUnlocked => '🎖️ Erfolg freigeschaltet!';
@override
String bonusPoints(int points) {
return '+$points Punkte ⚡';
}
@override
String checkInSuccess(int points) {
return 'Check-in erfolgreich! +$points Punkte ⚡';
}
@override
String get weeklyStreakBonus => '🎉 Wöchentlicher Streak-Bonus!';
@override
String get newAchievementUnlocked => '🎖️ Neuer Erfolg freigeschaltet!';
@override
String get alreadyCheckedIn =>
'Du hast heute bereits eingecheckt! Komm morgen wieder 📅';
@override
String get checkInCalendar => 'Check-in-Kalender 📅';
@override
String get checkInToday => '📅 Heute einchecken';
@override
String get checkedInToday => '✓ Heute eingecheckt';
@override
String get currentStreak => '🔥 Aktueller Streak';
@override
String get longestStreak => '🏆 Längster Streak';
@override
String get days => 'Tage';
@override
String daysCount(int count) {
return '$count Tage';
}
@override
String get achievements => 'Erfolge 🎖️';
@override
String get viewAllAchievements => 'Alle Erfolge anzeigen';
@override
String get allAchievementsComingSoon =>
'Vollständiger Erfolge-Bildschirm kommt bald!';
@override
String get profile => 'Profil';
@override
String get focuser => 'Fokussierer';
@override
String pointsToNextLevel(int points, int level) {
return '$points Punkte bis Level $level';
}
@override
String get achievement_first_session_name => 'Fokus-Neuling';
@override
String get achievement_first_session_desc =>
'Schließe deine erste Fokussitzung ab';
@override
String get achievement_sessions_10_name => 'Erste Schritte';
@override
String get achievement_sessions_10_desc => 'Schließe 10 Fokussitzungen ab';
@override
String get achievement_sessions_50_name => 'Fokus-Enthusiast';
@override
String get achievement_sessions_50_desc => 'Schließe 50 Fokussitzungen ab';
@override
String get achievement_sessions_100_name => 'Fokus-Meister';
@override
String get achievement_sessions_100_desc => 'Schließe 100 Fokussitzungen ab';
@override
String get achievement_honest_bronze_name => 'Ehrlicher Tracker · Bronze';
@override
String get achievement_honest_bronze_desc =>
'Zeichne 50 Ablenkungen ehrlich auf';
@override
String get achievement_honest_silver_name => 'Ehrlicher Tracker · Silber';
@override
String get achievement_honest_silver_desc =>
'Zeichne 200 Ablenkungen ehrlich auf';
@override
String get achievement_honest_gold_name => 'Ehrlicher Tracker · Gold';
@override
String get achievement_honest_gold_desc =>
'Zeichne 500 Ablenkungen ehrlich auf';
@override
String get achievement_marathon_name => 'Marathon-Läufer';
@override
String get achievement_marathon_desc => 'Sammle 10 Stunden Fokuszeit';
@override
String get achievement_century_name => 'Jahrhundert-Club';
@override
String get achievement_century_desc => 'Sammle 100 Stunden Fokuszeit';
@override
String get achievement_master_name => 'Fokus-Großmeister';
@override
String get achievement_master_desc => 'Sammle 1000 Stunden Fokuszeit';
@override
String get achievement_persistence_star_name => 'Beharrlichkeitsstern';
@override
String get achievement_persistence_star_desc => 'Checke 7 Tage in Folge ein';
@override
String get achievement_monthly_habit_name => 'Monatliche Gewohnheit';
@override
String get achievement_monthly_habit_desc => 'Checke 30 Tage in Folge ein';
@override
String get achievement_centurion_name => 'Zenturio';
@override
String get achievement_centurion_desc => 'Checke 100 Tage in Folge ein';
@override
String get achievement_year_warrior_name => 'Jahreskrieger';
@override
String get achievement_year_warrior_desc => 'Checke 365 Tage in Folge ein';
@override
String get total => 'Gesamt';
@override
String get status => 'Status';
@override
String get pointsBreakdown => 'Punkteaufschlüsselung';
@override
String get focusTimePoints => 'Fokuszeit';
@override
String get focusTimePointsDesc => '1 Punkt pro Minute Fokus';
@override
String get honestyBonusLabel => 'Ehrlichkeitsbonus';
@override
String get honestyBonusDesc =>
'Extrapunkte für das Aufzeichnen von Ablenkungen';
@override
String get checkInPoints => 'Täglicher Check-in';
@override
String get checkInPointsDesc => 'Basispunkte für täglichen Check-in';
@override
String get streakBonus => 'Streak-Bonus';
@override
String streakBonusDesc(int days) {
return '$days aufeinanderfolgende Check-ins';
}
@override
String get achievementBonusLabel => 'Erfolgsbonus';
@override
String get weekdayS => 'S';
@override
String get weekdayM => 'M';
@override
String get weekdayT => 'D';
@override
String get weekdayW => 'M';
@override
String get weekdayTh => 'D';
@override
String get weekdayF => 'F';
@override
String get weekdaySa => 'S';
} }

View File

@@ -334,4 +334,251 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get arabic => 'العربية (Arabic)'; String get arabic => 'العربية (Arabic)';
@override
String get points => 'Points';
@override
String get level => 'Level';
@override
String get checked => 'Checked';
@override
String get checkIn => 'Check In';
@override
String get earnedPoints => 'Earned:';
@override
String get basePoints => 'Base Points';
@override
String get honestyBonus => 'Honesty Bonus';
@override
String totalPoints(int count) {
return 'Total Points: $count';
}
@override
String distractionsRecorded(int count, Object distractionText) {
return '($count $distractionText recorded)';
}
@override
String get achievementUnlocked => '🎖️ Achievement Unlocked!';
@override
String bonusPoints(int points) {
return '+$points Points ⚡';
}
@override
String checkInSuccess(int points) {
return 'Check-in successful! +$points points ⚡';
}
@override
String get weeklyStreakBonus => '🎉 Weekly streak bonus!';
@override
String get newAchievementUnlocked => '🎖️ New achievement unlocked!';
@override
String get alreadyCheckedIn =>
'You have already checked in today! Come back tomorrow 📅';
@override
String get checkInCalendar => 'Check-In Calendar 📅';
@override
String get checkInToday => '📅 Check In Today';
@override
String get checkedInToday => '✓ Checked In Today';
@override
String get currentStreak => '🔥 Current Streak';
@override
String get longestStreak => '🏆 Longest Streak';
@override
String get days => 'days';
@override
String daysCount(int count) {
return '$count days';
}
@override
String get achievements => 'Achievements 🎖️';
@override
String get viewAllAchievements => 'View All Achievements';
@override
String get allAchievementsComingSoon =>
'Full achievements screen coming soon!';
@override
String get profile => 'Profile';
@override
String get focuser => 'Focuser';
@override
String pointsToNextLevel(int points, int level) {
return '$points points to Level $level';
}
@override
String get achievement_first_session_name => 'Focus Newbie';
@override
String get achievement_first_session_desc =>
'Complete your first focus session';
@override
String get achievement_sessions_10_name => 'Getting Started';
@override
String get achievement_sessions_10_desc => 'Complete 10 focus sessions';
@override
String get achievement_sessions_50_name => 'Focus Enthusiast';
@override
String get achievement_sessions_50_desc => 'Complete 50 focus sessions';
@override
String get achievement_sessions_100_name => 'Focus Master';
@override
String get achievement_sessions_100_desc => 'Complete 100 focus sessions';
@override
String get achievement_honest_bronze_name => 'Honest Tracker · Bronze';
@override
String get achievement_honest_bronze_desc =>
'Record 50 distractions honestly';
@override
String get achievement_honest_silver_name => 'Honest Tracker · Silver';
@override
String get achievement_honest_silver_desc =>
'Record 200 distractions honestly';
@override
String get achievement_honest_gold_name => 'Honest Tracker · Gold';
@override
String get achievement_honest_gold_desc => 'Record 500 distractions honestly';
@override
String get achievement_marathon_name => 'Marathon Runner';
@override
String get achievement_marathon_desc => 'Accumulate 10 hours of focus time';
@override
String get achievement_century_name => 'Century Club';
@override
String get achievement_century_desc => 'Accumulate 100 hours of focus time';
@override
String get achievement_master_name => 'Focus Grandmaster';
@override
String get achievement_master_desc => 'Accumulate 1000 hours of focus time';
@override
String get achievement_persistence_star_name => 'Persistence Star';
@override
String get achievement_persistence_star_desc =>
'Check in for 7 consecutive days';
@override
String get achievement_monthly_habit_name => 'Monthly Habit';
@override
String get achievement_monthly_habit_desc =>
'Check in for 30 consecutive days';
@override
String get achievement_centurion_name => 'Centurion';
@override
String get achievement_centurion_desc => 'Check in for 100 consecutive days';
@override
String get achievement_year_warrior_name => 'Year Warrior';
@override
String get achievement_year_warrior_desc =>
'Check in for 365 consecutive days';
@override
String get total => 'Total';
@override
String get status => 'Status';
@override
String get pointsBreakdown => 'Points Breakdown';
@override
String get focusTimePoints => 'Focus Time';
@override
String get focusTimePointsDesc => '1 point per minute of focus';
@override
String get honestyBonusLabel => 'Honesty Bonus';
@override
String get honestyBonusDesc => 'Extra points for recording distractions';
@override
String get checkInPoints => 'Daily Check-In';
@override
String get checkInPointsDesc => 'Base points for daily check-in';
@override
String get streakBonus => 'Streak Bonus';
@override
String streakBonusDesc(int days) {
return '$days consecutive check-ins';
}
@override
String get achievementBonusLabel => 'Achievement Bonus';
@override
String get weekdayS => 'S';
@override
String get weekdayM => 'M';
@override
String get weekdayT => 'T';
@override
String get weekdayW => 'W';
@override
String get weekdayTh => 'T';
@override
String get weekdayF => 'F';
@override
String get weekdaySa => 'S';
} }

View File

@@ -337,4 +337,256 @@ class AppLocalizationsEs extends AppLocalizations {
@override @override
String get arabic => 'العربية'; String get arabic => 'العربية';
@override
String get points => 'Puntos';
@override
String get level => 'Nivel';
@override
String get checked => 'Registrado';
@override
String get checkIn => 'Registrarse';
@override
String get earnedPoints => 'Ganado:';
@override
String get basePoints => 'Puntos Base';
@override
String get honestyBonus => 'Bono de Honestidad';
@override
String totalPoints(int count) {
return 'Puntos Totales: $count';
}
@override
String distractionsRecorded(int count, Object distractionText) {
return '($count $distractionText registradas)';
}
@override
String get achievementUnlocked => '🎖️ ¡Logro Desbloqueado!';
@override
String bonusPoints(int points) {
return '+$points Puntos ⚡';
}
@override
String checkInSuccess(int points) {
return '¡Registro exitoso! +$points puntos ⚡';
}
@override
String get weeklyStreakBonus => '🎉 ¡Bono de racha semanal!';
@override
String get newAchievementUnlocked => '🎖️ ¡Nuevo logro desbloqueado!';
@override
String get alreadyCheckedIn => '¡Ya te registraste hoy! Vuelve mañana 📅';
@override
String get checkInCalendar => 'Calendario de Registro 📅';
@override
String get checkInToday => '📅 Registrarse Hoy';
@override
String get checkedInToday => '✓ Registrado Hoy';
@override
String get currentStreak => '🔥 Racha Actual';
@override
String get longestStreak => '🏆 Racha Más Larga';
@override
String get days => 'días';
@override
String daysCount(int count) {
return '$count días';
}
@override
String get achievements => 'Logros 🎖️';
@override
String get viewAllAchievements => 'Ver Todos los Logros';
@override
String get allAchievementsComingSoon =>
'¡Pantalla completa de logros próximamente!';
@override
String get profile => 'Perfil';
@override
String get focuser => 'Enfocador';
@override
String pointsToNextLevel(int points, int level) {
return '$points puntos para Nivel $level';
}
@override
String get achievement_first_session_name => 'Novato del Enfoque';
@override
String get achievement_first_session_desc =>
'Completa tu primera sesión de enfoque';
@override
String get achievement_sessions_10_name => 'Comenzando';
@override
String get achievement_sessions_10_desc => 'Completa 10 sesiones de enfoque';
@override
String get achievement_sessions_50_name => 'Entusiasta del Enfoque';
@override
String get achievement_sessions_50_desc => 'Completa 50 sesiones de enfoque';
@override
String get achievement_sessions_100_name => 'Maestro del Enfoque';
@override
String get achievement_sessions_100_desc =>
'Completa 100 sesiones de enfoque';
@override
String get achievement_honest_bronze_name => 'Registrador Honesto · Bronce';
@override
String get achievement_honest_bronze_desc =>
'Registra 50 distracciones honestamente';
@override
String get achievement_honest_silver_name => 'Registrador Honesto · Plata';
@override
String get achievement_honest_silver_desc =>
'Registra 200 distracciones honestamente';
@override
String get achievement_honest_gold_name => 'Registrador Honesto · Oro';
@override
String get achievement_honest_gold_desc =>
'Registra 500 distracciones honestamente';
@override
String get achievement_marathon_name => 'Corredor de Maratón';
@override
String get achievement_marathon_desc =>
'Acumula 10 horas de tiempo de enfoque';
@override
String get achievement_century_name => 'Club del Siglo';
@override
String get achievement_century_desc =>
'Acumula 100 horas de tiempo de enfoque';
@override
String get achievement_master_name => 'Gran Maestro del Enfoque';
@override
String get achievement_master_desc =>
'Acumula 1000 horas de tiempo de enfoque';
@override
String get achievement_persistence_star_name => 'Estrella de Persistencia';
@override
String get achievement_persistence_star_desc =>
'Regístrate durante 7 días consecutivos';
@override
String get achievement_monthly_habit_name => 'Hábito Mensual';
@override
String get achievement_monthly_habit_desc =>
'Regístrate durante 30 días consecutivos';
@override
String get achievement_centurion_name => 'Centurión';
@override
String get achievement_centurion_desc =>
'Regístrate durante 100 días consecutivos';
@override
String get achievement_year_warrior_name => 'Guerrero del Año';
@override
String get achievement_year_warrior_desc =>
'Regístrate durante 365 días consecutivos';
@override
String get total => 'Total';
@override
String get status => 'Estado';
@override
String get pointsBreakdown => 'Desglose de Puntos';
@override
String get focusTimePoints => 'Tiempo de Enfoque';
@override
String get focusTimePointsDesc => '1 punto por minuto de enfoque';
@override
String get honestyBonusLabel => 'Bono de Honestidad';
@override
String get honestyBonusDesc => 'Puntos extra por registrar distracciones';
@override
String get checkInPoints => 'Registro Diario';
@override
String get checkInPointsDesc => 'Puntos base por primer registro del día';
@override
String get streakBonus => 'Bono de Racha';
@override
String streakBonusDesc(int days) {
return '$days registros consecutivos';
}
@override
String get achievementBonusLabel => 'Bono de Logro';
@override
String get weekdayS => 'D';
@override
String get weekdayM => 'L';
@override
String get weekdayT => 'M';
@override
String get weekdayW => 'X';
@override
String get weekdayTh => 'J';
@override
String get weekdayF => 'V';
@override
String get weekdaySa => 'S';
} }

View File

@@ -337,4 +337,261 @@ class AppLocalizationsFr extends AppLocalizations {
@override @override
String get arabic => 'العربية'; String get arabic => 'العربية';
@override
String get points => 'Points';
@override
String get level => 'Niveau';
@override
String get checked => 'Vérifié';
@override
String get checkIn => 'S\'enregistrer';
@override
String get earnedPoints => 'Gagné:';
@override
String get basePoints => 'Points de base';
@override
String get honestyBonus => 'Bonus d\'honnêteté';
@override
String totalPoints(int count) {
return 'Total des points: $count';
}
@override
String distractionsRecorded(int count, Object distractionText) {
return '($count $distractionText enregistrées)';
}
@override
String get achievementUnlocked => '🎖️ Succès débloqué!';
@override
String bonusPoints(int points) {
return '+$points Points ⚡';
}
@override
String checkInSuccess(int points) {
return 'Enregistrement réussi! +$points points ⚡';
}
@override
String get weeklyStreakBonus => '🎉 Bonus de série hebdomadaire!';
@override
String get newAchievementUnlocked => '🎖️ Nouveau succès débloqué!';
@override
String get alreadyCheckedIn =>
'Vous vous êtes déjà enregistré aujourd\'hui! Revenez demain 📅';
@override
String get checkInCalendar => 'Calendrier d\'enregistrement 📅';
@override
String get checkInToday => '📅 S\'enregistrer aujourd\'hui';
@override
String get checkedInToday => '✓ Enregistré aujourd\'hui';
@override
String get currentStreak => '🔥 Série actuelle';
@override
String get longestStreak => '🏆 Plus longue série';
@override
String get days => 'jours';
@override
String daysCount(int count) {
return '$count jours';
}
@override
String get achievements => 'Succès 🎖️';
@override
String get viewAllAchievements => 'Voir tous les succès';
@override
String get allAchievementsComingSoon =>
'Écran complet des succès bientôt disponible!';
@override
String get profile => 'Profil';
@override
String get focuser => 'Concentrateur';
@override
String pointsToNextLevel(int points, int level) {
return '$points points jusqu\'au niveau $level';
}
@override
String get achievement_first_session_name => 'Débutant en concentration';
@override
String get achievement_first_session_desc =>
'Complétez votre première session de concentration';
@override
String get achievement_sessions_10_name => 'Premiers pas';
@override
String get achievement_sessions_10_desc =>
'Complétez 10 sessions de concentration';
@override
String get achievement_sessions_50_name => 'Passionné de concentration';
@override
String get achievement_sessions_50_desc =>
'Complétez 50 sessions de concentration';
@override
String get achievement_sessions_100_name => 'Maître de la concentration';
@override
String get achievement_sessions_100_desc =>
'Complétez 100 sessions de concentration';
@override
String get achievement_honest_bronze_name => 'Tracker honnête · Bronze';
@override
String get achievement_honest_bronze_desc =>
'Enregistrez 50 distractions honnêtement';
@override
String get achievement_honest_silver_name => 'Tracker honnête · Argent';
@override
String get achievement_honest_silver_desc =>
'Enregistrez 200 distractions honnêtement';
@override
String get achievement_honest_gold_name => 'Tracker honnête · Or';
@override
String get achievement_honest_gold_desc =>
'Enregistrez 500 distractions honnêtement';
@override
String get achievement_marathon_name => 'Coureur de marathon';
@override
String get achievement_marathon_desc =>
'Accumulez 10 heures de temps de concentration';
@override
String get achievement_century_name => 'Club du siècle';
@override
String get achievement_century_desc =>
'Accumulez 100 heures de temps de concentration';
@override
String get achievement_master_name => 'Grand maître de la concentration';
@override
String get achievement_master_desc =>
'Accumulez 1000 heures de temps de concentration';
@override
String get achievement_persistence_star_name => 'Étoile de la persévérance';
@override
String get achievement_persistence_star_desc =>
'Enregistrez-vous pendant 7 jours consécutifs';
@override
String get achievement_monthly_habit_name => 'Habitude mensuelle';
@override
String get achievement_monthly_habit_desc =>
'Enregistrez-vous pendant 30 jours consécutifs';
@override
String get achievement_centurion_name => 'Centurion';
@override
String get achievement_centurion_desc =>
'Enregistrez-vous pendant 100 jours consécutifs';
@override
String get achievement_year_warrior_name => 'Guerrier de l\'année';
@override
String get achievement_year_warrior_desc =>
'Enregistrez-vous pendant 365 jours consécutifs';
@override
String get total => 'Total';
@override
String get status => 'Statut';
@override
String get pointsBreakdown => 'Répartition des points';
@override
String get focusTimePoints => 'Temps de concentration';
@override
String get focusTimePointsDesc => '1 point par minute de concentration';
@override
String get honestyBonusLabel => 'Bonus d\'honnêteté';
@override
String get honestyBonusDesc =>
'Points supplémentaires pour l\'enregistrement des distractions';
@override
String get checkInPoints => 'Enregistrement quotidien';
@override
String get checkInPointsDesc =>
'Points de base pour l\'enregistrement quotidien';
@override
String get streakBonus => 'Bonus de série';
@override
String streakBonusDesc(int days) {
return '$days enregistrements consécutifs';
}
@override
String get achievementBonusLabel => 'Bonus de succès';
@override
String get weekdayS => 'D';
@override
String get weekdayM => 'L';
@override
String get weekdayT => 'M';
@override
String get weekdayW => 'M';
@override
String get weekdayTh => 'J';
@override
String get weekdayF => 'V';
@override
String get weekdaySa => 'S';
} }

View File

@@ -336,4 +336,249 @@ class AppLocalizationsHi extends AppLocalizations {
@override @override
String get arabic => 'العربية'; String get arabic => 'العربية';
@override
String get points => 'अंक';
@override
String get level => 'स्तर';
@override
String get checked => 'चेक किया';
@override
String get checkIn => 'चेक-इन';
@override
String get earnedPoints => 'अर्जित:';
@override
String get basePoints => 'मूल अंक';
@override
String get honestyBonus => 'ईमानदारी बोनस';
@override
String totalPoints(int count) {
return 'कुल अंक: $count';
}
@override
String distractionsRecorded(int count, Object distractionText) {
return '($count $distractionText रिकॉर्ड किया)';
}
@override
String get achievementUnlocked => '🎖️ उपलब्धि अनलॉक!';
@override
String bonusPoints(int points) {
return '+$points अंक ⚡';
}
@override
String checkInSuccess(int points) {
return 'चेक-इन सफल! +$points अंक ⚡';
}
@override
String get weeklyStreakBonus => '🎉 साप्ताहिक स्ट्रीक बोनस!';
@override
String get newAchievementUnlocked => '🎖️ नई उपलब्धि अनलॉक!';
@override
String get alreadyCheckedIn =>
'आप आज पहले ही चेक-इन कर चुके हैं! कल वापस आएं 📅';
@override
String get checkInCalendar => 'चेक-इन कैलेंडर 📅';
@override
String get checkInToday => '📅 आज चेक-इन करें';
@override
String get checkedInToday => '✓ आज चेक-इन हो गया';
@override
String get currentStreak => '🔥 वर्तमान स्ट्रीक';
@override
String get longestStreak => '🏆 सबसे लंबी स्ट्रीक';
@override
String get days => 'दिन';
@override
String daysCount(int count) {
return '$count दिन';
}
@override
String get achievements => 'उपलब्धियाँ 🎖️';
@override
String get viewAllAchievements => 'सभी उपलब्धियाँ देखें';
@override
String get allAchievementsComingSoon =>
'पूर्ण उपलब्धि स्क्रीन जल्द आ रही है!';
@override
String get profile => 'प्रोफ़ाइल';
@override
String get focuser => 'फोकस करने वाला';
@override
String pointsToNextLevel(int points, int level) {
return 'स्तर $level के लिए $points अंक';
}
@override
String get achievement_first_session_name => 'फोकस नौसिखिया';
@override
String get achievement_first_session_desc => 'अपना पहला फोकस सत्र पूरा करें';
@override
String get achievement_sessions_10_name => 'शुरुआत';
@override
String get achievement_sessions_10_desc => '10 फोकस सत्र पूरे करें';
@override
String get achievement_sessions_50_name => 'फोकस उत्साही';
@override
String get achievement_sessions_50_desc => '50 फोकस सत्र पूरे करें';
@override
String get achievement_sessions_100_name => 'फोकस मास्टर';
@override
String get achievement_sessions_100_desc => '100 फोकस सत्र पूरे करें';
@override
String get achievement_honest_bronze_name => 'ईमानदार ट्रैकर · कांस्य';
@override
String get achievement_honest_bronze_desc =>
'ईमानदारी से 50 विकर्षण रिकॉर्ड करें';
@override
String get achievement_honest_silver_name => 'ईमानदार ट्रैकर · रजत';
@override
String get achievement_honest_silver_desc =>
'ईमानदारी से 200 विकर्षण रिकॉर्ड करें';
@override
String get achievement_honest_gold_name => 'ईमानदार ट्रैकर · स्वर्ण';
@override
String get achievement_honest_gold_desc =>
'ईमानदारी से 500 विकर्षण रिकॉर्ड करें';
@override
String get achievement_marathon_name => 'मैराथन धावक';
@override
String get achievement_marathon_desc => '10 घंटे का फोकस समय जमा करें';
@override
String get achievement_century_name => 'सेंचुरी क्लब';
@override
String get achievement_century_desc => '100 घंटे का फोकस समय जमा करें';
@override
String get achievement_master_name => 'फोकस ग्रैंडमास्टर';
@override
String get achievement_master_desc => '1000 घंटे का फोकस समय जमा करें';
@override
String get achievement_persistence_star_name => 'दृढ़ता का सितारा';
@override
String get achievement_persistence_star_desc =>
'7 दिनों तक लगातार चेक-इन करें';
@override
String get achievement_monthly_habit_name => 'मासिक आदत';
@override
String get achievement_monthly_habit_desc => '30 दिनों तक लगातार चेक-इन करें';
@override
String get achievement_centurion_name => 'सेंचुरियन';
@override
String get achievement_centurion_desc => '100 दिनों तक लगातार चेक-इन करें';
@override
String get achievement_year_warrior_name => 'वर्ष योद्धा';
@override
String get achievement_year_warrior_desc => '365 दिनों तक लगातार चेक-इन करें';
@override
String get total => 'कुल';
@override
String get status => 'स्थिति';
@override
String get pointsBreakdown => 'अंकों का विवरण';
@override
String get focusTimePoints => 'फोकस समय';
@override
String get focusTimePointsDesc => 'फोकस के प्रति मिनट 1 अंक';
@override
String get honestyBonusLabel => 'ईमानदारी बोनस';
@override
String get honestyBonusDesc => 'विकर्षण रिकॉर्ड करने के लिए अतिरिक्त अंक';
@override
String get checkInPoints => 'दैनिक चेक-इन';
@override
String get checkInPointsDesc => 'दैनिक चेक-इन के लिए मूल अंक';
@override
String get streakBonus => 'स्ट्रीक बोनस';
@override
String streakBonusDesc(int days) {
return '$days लगातार चेक-इन';
}
@override
String get achievementBonusLabel => 'उपलब्धि बोनस';
@override
String get weekdayS => '';
@override
String get weekdayM => 'सो';
@override
String get weekdayT => 'मं';
@override
String get weekdayW => 'बु';
@override
String get weekdayTh => 'गु';
@override
String get weekdayF => 'शु';
@override
String get weekdaySa => '';
} }

View File

@@ -336,4 +336,251 @@ class AppLocalizationsId extends AppLocalizations {
@override @override
String get arabic => 'العربية'; String get arabic => 'العربية';
@override
String get points => 'Poin';
@override
String get level => 'Level';
@override
String get checked => 'Tercatat';
@override
String get checkIn => 'Check-in';
@override
String get earnedPoints => 'Diperoleh:';
@override
String get basePoints => 'Poin Dasar';
@override
String get honestyBonus => 'Bonus Kejujuran';
@override
String totalPoints(int count) {
return 'Total Poin: $count';
}
@override
String distractionsRecorded(int count, Object distractionText) {
return '($count $distractionText tercatat)';
}
@override
String get achievementUnlocked => '🎖️ Pencapaian Terbuka!';
@override
String bonusPoints(int points) {
return '+$points Poin ⚡';
}
@override
String checkInSuccess(int points) {
return 'Check-in berhasil! +$points poin ⚡';
}
@override
String get weeklyStreakBonus => '🎉 Bonus streak mingguan!';
@override
String get newAchievementUnlocked => '🎖️ Pencapaian baru terbuka!';
@override
String get alreadyCheckedIn =>
'Anda sudah check-in hari ini! Kembali lagi besok 📅';
@override
String get checkInCalendar => 'Kalender Check-In 📅';
@override
String get checkInToday => '📅 Check-in Hari Ini';
@override
String get checkedInToday => '✓ Sudah Check-in Hari Ini';
@override
String get currentStreak => '🔥 Streak Saat Ini';
@override
String get longestStreak => '🏆 Streak Terpanjang';
@override
String get days => 'hari';
@override
String daysCount(int count) {
return '$count hari';
}
@override
String get achievements => 'Pencapaian 🎖️';
@override
String get viewAllAchievements => 'Lihat Semua Pencapaian';
@override
String get allAchievementsComingSoon =>
'Layar pencapaian lengkap segera hadir!';
@override
String get profile => 'Profil';
@override
String get focuser => 'Pemfokus';
@override
String pointsToNextLevel(int points, int level) {
return '$points poin menuju Level $level';
}
@override
String get achievement_first_session_name => 'Pemula Fokus';
@override
String get achievement_first_session_desc =>
'Selesaikan sesi fokus pertama Anda';
@override
String get achievement_sessions_10_name => 'Memulai';
@override
String get achievement_sessions_10_desc => 'Selesaikan 10 sesi fokus';
@override
String get achievement_sessions_50_name => 'Penggemar Fokus';
@override
String get achievement_sessions_50_desc => 'Selesaikan 50 sesi fokus';
@override
String get achievement_sessions_100_name => 'Master Fokus';
@override
String get achievement_sessions_100_desc => 'Selesaikan 100 sesi fokus';
@override
String get achievement_honest_bronze_name => 'Pelacak Jujur · Perunggu';
@override
String get achievement_honest_bronze_desc => 'Catat 50 gangguan dengan jujur';
@override
String get achievement_honest_silver_name => 'Pelacak Jujur · Perak';
@override
String get achievement_honest_silver_desc =>
'Catat 200 gangguan dengan jujur';
@override
String get achievement_honest_gold_name => 'Pelacak Jujur · Emas';
@override
String get achievement_honest_gold_desc => 'Catat 500 gangguan dengan jujur';
@override
String get achievement_marathon_name => 'Pelari Maraton';
@override
String get achievement_marathon_desc => 'Kumpulkan 10 jam waktu fokus';
@override
String get achievement_century_name => 'Klub Abad';
@override
String get achievement_century_desc => 'Kumpulkan 100 jam waktu fokus';
@override
String get achievement_master_name => 'Grandmaster Fokus';
@override
String get achievement_master_desc => 'Kumpulkan 1000 jam waktu fokus';
@override
String get achievement_persistence_star_name => 'Bintang Kegigihan';
@override
String get achievement_persistence_star_desc =>
'Check-in selama 7 hari berturut-turut';
@override
String get achievement_monthly_habit_name => 'Kebiasaan Bulanan';
@override
String get achievement_monthly_habit_desc =>
'Check-in selama 30 hari berturut-turut';
@override
String get achievement_centurion_name => 'Centurion';
@override
String get achievement_centurion_desc =>
'Check-in selama 100 hari berturut-turut';
@override
String get achievement_year_warrior_name => 'Pejuang Tahun';
@override
String get achievement_year_warrior_desc =>
'Check-in selama 365 hari berturut-turut';
@override
String get total => 'Total';
@override
String get status => 'Status';
@override
String get pointsBreakdown => 'Rincian Poin';
@override
String get focusTimePoints => 'Waktu Fokus';
@override
String get focusTimePointsDesc => '1 poin per menit fokus';
@override
String get honestyBonusLabel => 'Bonus Kejujuran';
@override
String get honestyBonusDesc => 'Poin tambahan untuk mencatat gangguan';
@override
String get checkInPoints => 'Check-in Harian';
@override
String get checkInPointsDesc => 'Poin dasar untuk check-in harian';
@override
String get streakBonus => 'Bonus Streak';
@override
String streakBonusDesc(int days) {
return '$days check-in berturut-turut';
}
@override
String get achievementBonusLabel => 'Bonus Pencapaian';
@override
String get weekdayS => 'M';
@override
String get weekdayM => 'S';
@override
String get weekdayT => 'S';
@override
String get weekdayW => 'R';
@override
String get weekdayTh => 'K';
@override
String get weekdayF => 'J';
@override
String get weekdaySa => 'S';
} }

View File

@@ -338,4 +338,260 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String get arabic => 'العربية'; String get arabic => 'العربية';
@override
String get points => 'Punti';
@override
String get level => 'Livello';
@override
String get checked => 'Registrato';
@override
String get checkIn => 'Check-in';
@override
String get earnedPoints => 'Guadagnato:';
@override
String get basePoints => 'Punti Base';
@override
String get honestyBonus => 'Bonus Onestà';
@override
String totalPoints(int count) {
return 'Punti Totali: $count';
}
@override
String distractionsRecorded(int count, Object distractionText) {
return '($count $distractionText registrate)';
}
@override
String get achievementUnlocked => '🎖️ Obiettivo Sbloccato!';
@override
String bonusPoints(int points) {
return '+$points Punti ⚡';
}
@override
String checkInSuccess(int points) {
return 'Check-in riuscito! +$points punti ⚡';
}
@override
String get weeklyStreakBonus => '🎉 Bonus serie settimanale!';
@override
String get newAchievementUnlocked => '🎖️ Nuovo obiettivo sbloccato!';
@override
String get alreadyCheckedIn =>
'Hai già fatto il check-in oggi! Torna domani 📅';
@override
String get checkInCalendar => 'Calendario Check-In 📅';
@override
String get checkInToday => '📅 Check-in Oggi';
@override
String get checkedInToday => '✓ Check-in Fatto Oggi';
@override
String get currentStreak => '🔥 Serie Attuale';
@override
String get longestStreak => '🏆 Serie Più Lunga';
@override
String get days => 'giorni';
@override
String daysCount(int count) {
return '$count giorni';
}
@override
String get achievements => 'Obiettivi 🎖️';
@override
String get viewAllAchievements => 'Vedi Tutti gli Obiettivi';
@override
String get allAchievementsComingSoon =>
'Schermata completa degli obiettivi in arrivo!';
@override
String get profile => 'Profilo';
@override
String get focuser => 'Concentratore';
@override
String pointsToNextLevel(int points, int level) {
return '$points punti al Livello $level';
}
@override
String get achievement_first_session_name =>
'Principiante della Concentrazione';
@override
String get achievement_first_session_desc =>
'Completa la tua prima sessione di concentrazione';
@override
String get achievement_sessions_10_name => 'Inizio';
@override
String get achievement_sessions_10_desc =>
'Completa 10 sessioni di concentrazione';
@override
String get achievement_sessions_50_name => 'Appassionato di Concentrazione';
@override
String get achievement_sessions_50_desc =>
'Completa 50 sessioni di concentrazione';
@override
String get achievement_sessions_100_name => 'Maestro della Concentrazione';
@override
String get achievement_sessions_100_desc =>
'Completa 100 sessioni di concentrazione';
@override
String get achievement_honest_bronze_name => 'Tracker Onesto · Bronzo';
@override
String get achievement_honest_bronze_desc =>
'Registra onestamente 50 distrazioni';
@override
String get achievement_honest_silver_name => 'Tracker Onesto · Argento';
@override
String get achievement_honest_silver_desc =>
'Registra onestamente 200 distrazioni';
@override
String get achievement_honest_gold_name => 'Tracker Onesto · Oro';
@override
String get achievement_honest_gold_desc =>
'Registra onestamente 500 distrazioni';
@override
String get achievement_marathon_name => 'Maratoneta';
@override
String get achievement_marathon_desc =>
'Accumula 10 ore di tempo di concentrazione';
@override
String get achievement_century_name => 'Club del Secolo';
@override
String get achievement_century_desc =>
'Accumula 100 ore di tempo di concentrazione';
@override
String get achievement_master_name => 'Gran Maestro della Concentrazione';
@override
String get achievement_master_desc =>
'Accumula 1000 ore di tempo di concentrazione';
@override
String get achievement_persistence_star_name => 'Stella della Persistenza';
@override
String get achievement_persistence_star_desc =>
'Fai il check-in per 7 giorni consecutivi';
@override
String get achievement_monthly_habit_name => 'Abitudine Mensile';
@override
String get achievement_monthly_habit_desc =>
'Fai il check-in per 30 giorni consecutivi';
@override
String get achievement_centurion_name => 'Centurione';
@override
String get achievement_centurion_desc =>
'Fai il check-in per 100 giorni consecutivi';
@override
String get achievement_year_warrior_name => 'Guerriero dell\'Anno';
@override
String get achievement_year_warrior_desc =>
'Fai il check-in per 365 giorni consecutivi';
@override
String get total => 'Totale';
@override
String get status => 'Stato';
@override
String get pointsBreakdown => 'Dettaglio Punti';
@override
String get focusTimePoints => 'Tempo di Concentrazione';
@override
String get focusTimePointsDesc => '1 punto per minuto di concentrazione';
@override
String get honestyBonusLabel => 'Bonus Onestà';
@override
String get honestyBonusDesc => 'Punti extra per registrare distrazioni';
@override
String get checkInPoints => 'Check-in Giornaliero';
@override
String get checkInPointsDesc => 'Punti base per check-in giornaliero';
@override
String get streakBonus => 'Bonus Serie';
@override
String streakBonusDesc(int days) {
return '$days check-in consecutivi';
}
@override
String get achievementBonusLabel => 'Bonus Obiettivo';
@override
String get weekdayS => 'D';
@override
String get weekdayM => 'L';
@override
String get weekdayT => 'M';
@override
String get weekdayW => 'M';
@override
String get weekdayTh => 'G';
@override
String get weekdayF => 'V';
@override
String get weekdaySa => 'S';
} }

View File

@@ -329,4 +329,243 @@ class AppLocalizationsJa extends AppLocalizations {
@override @override
String get arabic => 'العربية'; String get arabic => 'العربية';
@override
String get points => 'ポイント';
@override
String get level => 'レベル';
@override
String get checked => 'チェック済み';
@override
String get checkIn => 'チェックイン';
@override
String get earnedPoints => '獲得:';
@override
String get basePoints => '基本ポイント';
@override
String get honestyBonus => '正直ボーナス';
@override
String totalPoints(int count) {
return '合計ポイント:$count';
}
@override
String distractionsRecorded(int count, Object distractionText) {
return '($count $distractionText 記録済み)';
}
@override
String get achievementUnlocked => '🎖️ 実績解除!';
@override
String bonusPoints(int points) {
return '+$points ポイント ⚡';
}
@override
String checkInSuccess(int points) {
return 'チェックイン成功!+$points ポイント ⚡';
}
@override
String get weeklyStreakBonus => '🎉 1週間連続ボーナス';
@override
String get newAchievementUnlocked => '🎖️ 新しい実績解除!';
@override
String get alreadyCheckedIn => '今日は既にチェックイン済みです!明日また来てください 📅';
@override
String get checkInCalendar => 'チェックインカレンダー 📅';
@override
String get checkInToday => '📅 今日チェックイン';
@override
String get checkedInToday => '✓ 今日チェックイン済み';
@override
String get currentStreak => '🔥 現在の連続';
@override
String get longestStreak => '🏆 最長連続';
@override
String get days => '';
@override
String daysCount(int count) {
return '$count';
}
@override
String get achievements => '実績 🎖️';
@override
String get viewAllAchievements => 'すべての実績を見る';
@override
String get allAchievementsComingSoon => '完全な実績画面は近日公開!';
@override
String get profile => 'プロフィール';
@override
String get focuser => '集中する人';
@override
String pointsToNextLevel(int points, int level) {
return 'レベル $level まであと $points ポイント';
}
@override
String get achievement_first_session_name => '集中初心者';
@override
String get achievement_first_session_desc => '最初の集中セッションを完了';
@override
String get achievement_sessions_10_name => '入門者';
@override
String get achievement_sessions_10_desc => '10回の集中セッションを完了';
@override
String get achievement_sessions_50_name => '集中愛好家';
@override
String get achievement_sessions_50_desc => '50回の集中セッションを完了';
@override
String get achievement_sessions_100_name => '集中マスター';
@override
String get achievement_sessions_100_desc => '100回の集中セッションを完了';
@override
String get achievement_honest_bronze_name => '正直な記録者・ブロンズ';
@override
String get achievement_honest_bronze_desc => '50回の気の散りを正直に記録';
@override
String get achievement_honest_silver_name => '正直な記録者・シルバー';
@override
String get achievement_honest_silver_desc => '200回の気の散りを正直に記録';
@override
String get achievement_honest_gold_name => '正直な記録者・ゴールド';
@override
String get achievement_honest_gold_desc => '500回の気の散りを正直に記録';
@override
String get achievement_marathon_name => 'マラソンランナー';
@override
String get achievement_marathon_desc => '10時間の集中時間を累積';
@override
String get achievement_century_name => 'センチュリークラブ';
@override
String get achievement_century_desc => '100時間の集中時間を累積';
@override
String get achievement_master_name => '集中グランドマスター';
@override
String get achievement_master_desc => '1000時間の集中時間を累積';
@override
String get achievement_persistence_star_name => '継続の星';
@override
String get achievement_persistence_star_desc => '7日間連続でチェックイン';
@override
String get achievement_monthly_habit_name => '月間習慣';
@override
String get achievement_monthly_habit_desc => '30日間連続でチェックイン';
@override
String get achievement_centurion_name => '百日戦士';
@override
String get achievement_centurion_desc => '100日間連続でチェックイン';
@override
String get achievement_year_warrior_name => '年間戦士';
@override
String get achievement_year_warrior_desc => '365日間連続でチェックイン';
@override
String get total => '合計';
@override
String get status => 'ステータス';
@override
String get pointsBreakdown => 'ポイント内訳';
@override
String get focusTimePoints => '集中時間';
@override
String get focusTimePointsDesc => '1分の集中につき1ポイント';
@override
String get honestyBonusLabel => '正直ボーナス';
@override
String get honestyBonusDesc => '気の散りを記録すると追加ポイント';
@override
String get checkInPoints => '毎日チェックイン';
@override
String get checkInPointsDesc => '毎日の初回チェックインで基本ポイント';
@override
String get streakBonus => '連続ボーナス';
@override
String streakBonusDesc(int days) {
return '$days 日連続チェックイン';
}
@override
String get achievementBonusLabel => '実績ボーナス';
@override
String get weekdayS => '';
@override
String get weekdayM => '';
@override
String get weekdayT => '';
@override
String get weekdayW => '';
@override
String get weekdayTh => '';
@override
String get weekdayF => '';
@override
String get weekdaySa => '';
} }

View File

@@ -330,4 +330,243 @@ class AppLocalizationsKo extends AppLocalizations {
@override @override
String get arabic => 'العربية'; String get arabic => 'العربية';
@override
String get points => '포인트';
@override
String get level => '레벨';
@override
String get checked => '체크 완료';
@override
String get checkIn => '체크인';
@override
String get earnedPoints => '획득:';
@override
String get basePoints => '기본 포인트';
@override
String get honestyBonus => '정직 보너스';
@override
String totalPoints(int count) {
return '총 포인트: $count';
}
@override
String distractionsRecorded(int count, Object distractionText) {
return '($count $distractionText 기록됨)';
}
@override
String get achievementUnlocked => '🎖️ 업적 달성!';
@override
String bonusPoints(int points) {
return '+$points 포인트 ⚡';
}
@override
String checkInSuccess(int points) {
return '체크인 성공! +$points 포인트 ⚡';
}
@override
String get weeklyStreakBonus => '🎉 주간 연속 보너스!';
@override
String get newAchievementUnlocked => '🎖️ 새로운 업적 달성!';
@override
String get alreadyCheckedIn => '오늘 이미 체크인했어요! 내일 다시 오세요 📅';
@override
String get checkInCalendar => '체크인 캘린더 📅';
@override
String get checkInToday => '📅 오늘 체크인';
@override
String get checkedInToday => '✓ 오늘 체크인 완료';
@override
String get currentStreak => '🔥 현재 연속';
@override
String get longestStreak => '🏆 최장 연속';
@override
String get days => '';
@override
String daysCount(int count) {
return '$count';
}
@override
String get achievements => '업적 🎖️';
@override
String get viewAllAchievements => '모든 업적 보기';
@override
String get allAchievementsComingSoon => '전체 업적 화면 곧 공개!';
@override
String get profile => '프로필';
@override
String get focuser => '집중하는 사람';
@override
String pointsToNextLevel(int points, int level) {
return '레벨 $level까지 $points 포인트 남음';
}
@override
String get achievement_first_session_name => '집중 초보자';
@override
String get achievement_first_session_desc => '첫 집중 세션 완료';
@override
String get achievement_sessions_10_name => '시작 단계';
@override
String get achievement_sessions_10_desc => '10회 집중 세션 완료';
@override
String get achievement_sessions_50_name => '집중 애호가';
@override
String get achievement_sessions_50_desc => '50회 집중 세션 완료';
@override
String get achievement_sessions_100_name => '집중 마스터';
@override
String get achievement_sessions_100_desc => '100회 집중 세션 완료';
@override
String get achievement_honest_bronze_name => '정직한 기록자 · 브론즈';
@override
String get achievement_honest_bronze_desc => '50회 산만함을 정직하게 기록';
@override
String get achievement_honest_silver_name => '정직한 기록자 · 실버';
@override
String get achievement_honest_silver_desc => '200회 산만함을 정직하게 기록';
@override
String get achievement_honest_gold_name => '정직한 기록자 · 골드';
@override
String get achievement_honest_gold_desc => '500회 산만함을 정직하게 기록';
@override
String get achievement_marathon_name => '마라톤 러너';
@override
String get achievement_marathon_desc => '누적 10시간 집중';
@override
String get achievement_century_name => '센추리 클럽';
@override
String get achievement_century_desc => '누적 100시간 집중';
@override
String get achievement_master_name => '집중 그랜드마스터';
@override
String get achievement_master_desc => '누적 1000시간 집중';
@override
String get achievement_persistence_star_name => '끈기의 별';
@override
String get achievement_persistence_star_desc => '7일 연속 체크인';
@override
String get achievement_monthly_habit_name => '월간 습관';
@override
String get achievement_monthly_habit_desc => '30일 연속 체크인';
@override
String get achievement_centurion_name => '백일 전사';
@override
String get achievement_centurion_desc => '100일 연속 체크인';
@override
String get achievement_year_warrior_name => '연간 전사';
@override
String get achievement_year_warrior_desc => '365일 연속 체크인';
@override
String get total => '합계';
@override
String get status => '상태';
@override
String get pointsBreakdown => '포인트 세부 내역';
@override
String get focusTimePoints => '집중 시간';
@override
String get focusTimePointsDesc => '1분 집중당 1포인트';
@override
String get honestyBonusLabel => '정직 보너스';
@override
String get honestyBonusDesc => '산만함 기록 시 추가 포인트';
@override
String get checkInPoints => '일일 체크인';
@override
String get checkInPointsDesc => '매일 첫 체크인 시 기본 포인트';
@override
String get streakBonus => '연속 보너스';
@override
String streakBonusDesc(int days) {
return '$days일 연속 체크인';
}
@override
String get achievementBonusLabel => '업적 보너스';
@override
String get weekdayS => '';
@override
String get weekdayM => '';
@override
String get weekdayT => '';
@override
String get weekdayW => '';
@override
String get weekdayTh => '';
@override
String get weekdayF => '';
@override
String get weekdaySa => '';
} }

View File

@@ -335,4 +335,252 @@ class AppLocalizationsPt extends AppLocalizations {
@override @override
String get arabic => 'العربية'; String get arabic => 'العربية';
@override
String get points => 'Pontos';
@override
String get level => 'Nível';
@override
String get checked => 'Verificado';
@override
String get checkIn => 'Check-in';
@override
String get earnedPoints => 'Ganhou:';
@override
String get basePoints => 'Pontos base';
@override
String get honestyBonus => 'Bônus de honestidade';
@override
String totalPoints(int count) {
return 'Total de pontos: $count';
}
@override
String distractionsRecorded(int count, Object distractionText) {
return '($count $distractionText registradas)';
}
@override
String get achievementUnlocked => '🎖️ Conquista desbloqueada!';
@override
String bonusPoints(int points) {
return '+$points Pontos ⚡';
}
@override
String checkInSuccess(int points) {
return 'Check-in bem-sucedido! +$points pontos ⚡';
}
@override
String get weeklyStreakBonus => '🎉 Bônus de sequência semanal!';
@override
String get newAchievementUnlocked => '🎖️ Nova conquista desbloqueada!';
@override
String get alreadyCheckedIn => 'Você já fez check-in hoje! Volte amanhã 📅';
@override
String get checkInCalendar => 'Calendário de check-in 📅';
@override
String get checkInToday => '📅 Fazer check-in hoje';
@override
String get checkedInToday => '✓ Check-in feito hoje';
@override
String get currentStreak => '🔥 Sequência atual';
@override
String get longestStreak => '🏆 Maior sequência';
@override
String get days => 'dias';
@override
String daysCount(int count) {
return '$count dias';
}
@override
String get achievements => 'Conquistas 🎖️';
@override
String get viewAllAchievements => 'Ver todas as conquistas';
@override
String get allAchievementsComingSoon =>
'Tela completa de conquistas em breve!';
@override
String get profile => 'Perfil';
@override
String get focuser => 'Focador';
@override
String pointsToNextLevel(int points, int level) {
return '$points pontos até o nível $level';
}
@override
String get achievement_first_session_name => 'Novato em foco';
@override
String get achievement_first_session_desc =>
'Complete sua primeira sessão de foco';
@override
String get achievement_sessions_10_name => 'Começando';
@override
String get achievement_sessions_10_desc => 'Complete 10 sessões de foco';
@override
String get achievement_sessions_50_name => 'Entusiasta do foco';
@override
String get achievement_sessions_50_desc => 'Complete 50 sessões de foco';
@override
String get achievement_sessions_100_name => 'Mestre do foco';
@override
String get achievement_sessions_100_desc => 'Complete 100 sessões de foco';
@override
String get achievement_honest_bronze_name => 'Rastreador honesto · Bronze';
@override
String get achievement_honest_bronze_desc =>
'Registre 50 distrações honestamente';
@override
String get achievement_honest_silver_name => 'Rastreador honesto · Prata';
@override
String get achievement_honest_silver_desc =>
'Registre 200 distrações honestamente';
@override
String get achievement_honest_gold_name => 'Rastreador honesto · Ouro';
@override
String get achievement_honest_gold_desc =>
'Registre 500 distrações honestamente';
@override
String get achievement_marathon_name => 'Corredor de maratona';
@override
String get achievement_marathon_desc => 'Acumule 10 horas de tempo de foco';
@override
String get achievement_century_name => 'Clube do século';
@override
String get achievement_century_desc => 'Acumule 100 horas de tempo de foco';
@override
String get achievement_master_name => 'Grão-mestre do foco';
@override
String get achievement_master_desc => 'Acumule 1000 horas de tempo de foco';
@override
String get achievement_persistence_star_name => 'Estrela da persistência';
@override
String get achievement_persistence_star_desc =>
'Faça check-in por 7 dias consecutivos';
@override
String get achievement_monthly_habit_name => 'Hábito mensal';
@override
String get achievement_monthly_habit_desc =>
'Faça check-in por 30 dias consecutivos';
@override
String get achievement_centurion_name => 'Centurião';
@override
String get achievement_centurion_desc =>
'Faça check-in por 100 dias consecutivos';
@override
String get achievement_year_warrior_name => 'Guerreiro do ano';
@override
String get achievement_year_warrior_desc =>
'Faça check-in por 365 dias consecutivos';
@override
String get total => 'Total';
@override
String get status => 'Status';
@override
String get pointsBreakdown => 'Detalhamento de pontos';
@override
String get focusTimePoints => 'Tempo de foco';
@override
String get focusTimePointsDesc => '1 ponto por minuto de foco';
@override
String get honestyBonusLabel => 'Bônus de honestidade';
@override
String get honestyBonusDesc => 'Pontos extras por registrar distrações';
@override
String get checkInPoints => 'Check-in diário';
@override
String get checkInPointsDesc => 'Pontos base para check-in diário';
@override
String get streakBonus => 'Bônus de sequência';
@override
String streakBonusDesc(int days) {
return '$days check-ins consecutivos';
}
@override
String get achievementBonusLabel => 'Bônus de conquista';
@override
String get weekdayS => 'D';
@override
String get weekdayM => 'S';
@override
String get weekdayT => 'T';
@override
String get weekdayW => 'Q';
@override
String get weekdayTh => 'Q';
@override
String get weekdayF => 'S';
@override
String get weekdaySa => 'S';
} }

View File

@@ -341,4 +341,249 @@ class AppLocalizationsRu extends AppLocalizations {
@override @override
String get arabic => 'العربية'; String get arabic => 'العربية';
@override
String get points => 'Очки';
@override
String get level => 'Уровень';
@override
String get checked => 'Отмечено';
@override
String get checkIn => 'Отметиться';
@override
String get earnedPoints => 'Получено:';
@override
String get basePoints => 'Базовые очки';
@override
String get honestyBonus => 'Бонус за честность';
@override
String totalPoints(int count) {
return 'Всего очков: $count';
}
@override
String distractionsRecorded(int count, Object distractionText) {
return '($count $distractionText записано)';
}
@override
String get achievementUnlocked => '🎖️ Достижение разблокировано!';
@override
String bonusPoints(int points) {
return '+$points очков ⚡';
}
@override
String checkInSuccess(int points) {
return 'Отметка успешна! +$points очков ⚡';
}
@override
String get weeklyStreakBonus => '🎉 Бонус за недельную серию!';
@override
String get newAchievementUnlocked => '🎖️ Новое достижение разблокировано!';
@override
String get alreadyCheckedIn =>
'Вы уже отметились сегодня! Приходите завтра 📅';
@override
String get checkInCalendar => 'Календарь отметок 📅';
@override
String get checkInToday => '📅 Отметиться сегодня';
@override
String get checkedInToday => '✓ Отмечен сегодня';
@override
String get currentStreak => '🔥 Текущая серия';
@override
String get longestStreak => '🏆 Самая длинная серия';
@override
String get days => 'дней';
@override
String daysCount(int count) {
return '$count дней';
}
@override
String get achievements => 'Достижения 🎖️';
@override
String get viewAllAchievements => 'Посмотреть все достижения';
@override
String get allAchievementsComingSoon => 'Полный экран достижений скоро!';
@override
String get profile => 'Профиль';
@override
String get focuser => 'Сосредоточенный';
@override
String pointsToNextLevel(int points, int level) {
return '$points очков до уровня $level';
}
@override
String get achievement_first_session_name => 'Новичок фокуса';
@override
String get achievement_first_session_desc =>
'Завершите первую сессию фокусировки';
@override
String get achievement_sessions_10_name => 'Начало';
@override
String get achievement_sessions_10_desc => 'Завершите 10 сессий фокусировки';
@override
String get achievement_sessions_50_name => 'Энтузиаст фокуса';
@override
String get achievement_sessions_50_desc => 'Завершите 50 сессий фокусировки';
@override
String get achievement_sessions_100_name => 'Мастер фокуса';
@override
String get achievement_sessions_100_desc =>
'Завершите 100 сессий фокусировки';
@override
String get achievement_honest_bronze_name => 'Честный трекер · Бронза';
@override
String get achievement_honest_bronze_desc => 'Честно запишите 50 отвлечений';
@override
String get achievement_honest_silver_name => 'Честный трекер · Серебро';
@override
String get achievement_honest_silver_desc => 'Честно запишите 200 отвлечений';
@override
String get achievement_honest_gold_name => 'Честный трекер · Золото';
@override
String get achievement_honest_gold_desc => 'Честно запишите 500 отвлечений';
@override
String get achievement_marathon_name => 'Марафонец';
@override
String get achievement_marathon_desc =>
'Накопите 10 часов времени фокусировки';
@override
String get achievement_century_name => 'Клуб столетия';
@override
String get achievement_century_desc =>
'Накопите 100 часов времени фокусировки';
@override
String get achievement_master_name => 'Гроссмейстер фокуса';
@override
String get achievement_master_desc =>
'Накопите 1000 часов времени фокусировки';
@override
String get achievement_persistence_star_name => 'Звезда упорства';
@override
String get achievement_persistence_star_desc => 'Отмечайтесь 7 дней подряд';
@override
String get achievement_monthly_habit_name => 'Месячная привычка';
@override
String get achievement_monthly_habit_desc => 'Отмечайтесь 30 дней подряд';
@override
String get achievement_centurion_name => 'Центурион';
@override
String get achievement_centurion_desc => 'Отмечайтесь 100 дней подряд';
@override
String get achievement_year_warrior_name => 'Воин года';
@override
String get achievement_year_warrior_desc => 'Отмечайтесь 365 дней подряд';
@override
String get total => 'Всего';
@override
String get status => 'Статус';
@override
String get pointsBreakdown => 'Разбивка очков';
@override
String get focusTimePoints => 'Время фокусировки';
@override
String get focusTimePointsDesc => '1 очко за минуту фокусировки';
@override
String get honestyBonusLabel => 'Бонус за честность';
@override
String get honestyBonusDesc => 'Дополнительные очки за запись отвлечений';
@override
String get checkInPoints => 'Ежедневная отметка';
@override
String get checkInPointsDesc => 'Базовые очки за ежедневную отметку';
@override
String get streakBonus => 'Бонус за серию';
@override
String streakBonusDesc(int days) {
return '$days дней подряд';
}
@override
String get achievementBonusLabel => 'Бонус за достижение';
@override
String get weekdayS => 'В';
@override
String get weekdayM => 'П';
@override
String get weekdayT => 'В';
@override
String get weekdayW => 'С';
@override
String get weekdayTh => 'Ч';
@override
String get weekdayF => 'П';
@override
String get weekdaySa => 'С';
} }

View File

@@ -326,4 +326,243 @@ class AppLocalizationsZh extends AppLocalizations {
@override @override
String get arabic => 'العربية'; String get arabic => 'العربية';
@override
String get points => '积分';
@override
String get level => '等级';
@override
String get checked => '已签到';
@override
String get checkIn => '签到';
@override
String get earnedPoints => '获得:';
@override
String get basePoints => '基础积分';
@override
String get honestyBonus => '诚实奖励';
@override
String totalPoints(int count) {
return '总积分:$count';
}
@override
String distractionsRecorded(int count, Object distractionText) {
return '($count $distractionText 已记录)';
}
@override
String get achievementUnlocked => '🎖️ 成就解锁!';
@override
String bonusPoints(int points) {
return '+$points 积分 ⚡';
}
@override
String checkInSuccess(int points) {
return '签到成功!+$points 积分 ⚡';
}
@override
String get weeklyStreakBonus => '🎉 连续签到一周奖励!';
@override
String get newAchievementUnlocked => '🎖️ 新成就解锁!';
@override
String get alreadyCheckedIn => '你今天已经签到过了!明天再来 📅';
@override
String get checkInCalendar => '签到日历 📅';
@override
String get checkInToday => '📅 今日签到';
@override
String get checkedInToday => '✓ 今日已签到';
@override
String get currentStreak => '🔥 当前连续';
@override
String get longestStreak => '🏆 最长连续';
@override
String get days => '';
@override
String daysCount(int count) {
return '$count';
}
@override
String get achievements => '成就 🎖️';
@override
String get viewAllAchievements => '查看所有成就';
@override
String get allAchievementsComingSoon => '完整成就页面即将推出!';
@override
String get profile => '个人资料';
@override
String get focuser => '专注者';
@override
String pointsToNextLevel(int points, int level) {
return '距离等级 $level 还需 $points 积分';
}
@override
String get achievement_first_session_name => '专注新手';
@override
String get achievement_first_session_desc => '完成首个专注会话';
@override
String get achievement_sessions_10_name => '初露锋芒';
@override
String get achievement_sessions_10_desc => '完成 10 次专注会话';
@override
String get achievement_sessions_50_name => '专注达人';
@override
String get achievement_sessions_50_desc => '完成 50 次专注会话';
@override
String get achievement_sessions_100_name => '专注大师';
@override
String get achievement_sessions_100_desc => '完成 100 次专注会话';
@override
String get achievement_honest_bronze_name => '诚实记录者·铜';
@override
String get achievement_honest_bronze_desc => '诚实记录 50 次分心';
@override
String get achievement_honest_silver_name => '诚实记录者·银';
@override
String get achievement_honest_silver_desc => '诚实记录 200 次分心';
@override
String get achievement_honest_gold_name => '诚实记录者·金';
@override
String get achievement_honest_gold_desc => '诚实记录 500 次分心';
@override
String get achievement_marathon_name => '马拉松跑者';
@override
String get achievement_marathon_desc => '累计专注 10 小时';
@override
String get achievement_century_name => '百时俱乐部';
@override
String get achievement_century_desc => '累计专注 100 小时';
@override
String get achievement_master_name => '专注宗师';
@override
String get achievement_master_desc => '累计专注 1000 小时';
@override
String get achievement_persistence_star_name => '坚持之星';
@override
String get achievement_persistence_star_desc => '连续签到 7 天';
@override
String get achievement_monthly_habit_name => '月度习惯';
@override
String get achievement_monthly_habit_desc => '连续签到 30 天';
@override
String get achievement_centurion_name => '百日勇士';
@override
String get achievement_centurion_desc => '连续签到 100 天';
@override
String get achievement_year_warrior_name => '年度战士';
@override
String get achievement_year_warrior_desc => '连续签到 365 天';
@override
String get total => '总计';
@override
String get status => '状态';
@override
String get pointsBreakdown => '积分明细';
@override
String get focusTimePoints => '专注时长';
@override
String get focusTimePointsDesc => '每专注1分钟获得1积分';
@override
String get honestyBonusLabel => '诚实奖励';
@override
String get honestyBonusDesc => '记录分心情况获得额外积分';
@override
String get checkInPoints => '每日签到';
@override
String get checkInPointsDesc => '每日首次签到获得基础积分';
@override
String get streakBonus => '连续签到奖励';
@override
String streakBonusDesc(int days) {
return '连续签到 $days';
}
@override
String get achievementBonusLabel => '成就奖励';
@override
String get weekdayS => '';
@override
String get weekdayM => '';
@override
String get weekdayT => '';
@override
String get weekdayW => '';
@override
String get weekdayTh => '';
@override
String get weekdayF => '';
@override
String get weekdaySa => '';
} }

View File

@@ -121,5 +121,84 @@
"hindi": "हिन्दी", "hindi": "हिन्दी",
"indonesian": "Bahasa Indonesia", "indonesian": "Bahasa Indonesia",
"italian": "Italiano", "italian": "Italiano",
"arabic": "العربية" "arabic": "العربية",
"points": "Pontos",
"level": "Nível",
"checked": "Verificado",
"checkIn": "Check-in",
"earnedPoints": "Ganhou:",
"basePoints": "Pontos base",
"honestyBonus": "Bônus de honestidade",
"totalPoints": "Total de pontos: {count} ⚡",
"distractionsRecorded": "({count} {distractionText} registradas)",
"achievementUnlocked": "🎖️ Conquista desbloqueada!",
"bonusPoints": "+{points} Pontos ⚡",
"checkInSuccess": "Check-in bem-sucedido! +{points} pontos ⚡",
"weeklyStreakBonus": "🎉 Bônus de sequência semanal!",
"newAchievementUnlocked": "🎖️ Nova conquista desbloqueada!",
"alreadyCheckedIn": "Você já fez check-in hoje! Volte amanhã 📅",
"checkInCalendar": "Calendário de check-in 📅",
"checkInToday": "📅 Fazer check-in hoje",
"checkedInToday": "✓ Check-in feito hoje",
"currentStreak": "🔥 Sequência atual",
"longestStreak": "🏆 Maior sequência",
"days": "dias",
"daysCount": "{count} dias",
"achievements": "Conquistas 🎖️",
"viewAllAchievements": "Ver todas as conquistas",
"allAchievementsComingSoon": "Tela completa de conquistas em breve!",
"profile": "Perfil",
"focuser": "Focador",
"pointsToNextLevel": "{points} pontos até o nível {level}",
"achievement_first_session_name": "Novato em foco",
"achievement_first_session_desc": "Complete sua primeira sessão de foco",
"achievement_sessions_10_name": "Começando",
"achievement_sessions_10_desc": "Complete 10 sessões de foco",
"achievement_sessions_50_name": "Entusiasta do foco",
"achievement_sessions_50_desc": "Complete 50 sessões de foco",
"achievement_sessions_100_name": "Mestre do foco",
"achievement_sessions_100_desc": "Complete 100 sessões de foco",
"achievement_honest_bronze_name": "Rastreador honesto · Bronze",
"achievement_honest_bronze_desc": "Registre 50 distrações honestamente",
"achievement_honest_silver_name": "Rastreador honesto · Prata",
"achievement_honest_silver_desc": "Registre 200 distrações honestamente",
"achievement_honest_gold_name": "Rastreador honesto · Ouro",
"achievement_honest_gold_desc": "Registre 500 distrações honestamente",
"achievement_marathon_name": "Corredor de maratona",
"achievement_marathon_desc": "Acumule 10 horas de tempo de foco",
"achievement_century_name": "Clube do século",
"achievement_century_desc": "Acumule 100 horas de tempo de foco",
"achievement_master_name": "Grão-mestre do foco",
"achievement_master_desc": "Acumule 1000 horas de tempo de foco",
"achievement_persistence_star_name": "Estrela da persistência",
"achievement_persistence_star_desc": "Faça check-in por 7 dias consecutivos",
"achievement_monthly_habit_name": "Hábito mensal",
"achievement_monthly_habit_desc": "Faça check-in por 30 dias consecutivos",
"achievement_centurion_name": "Centurião",
"achievement_centurion_desc": "Faça check-in por 100 dias consecutivos",
"achievement_year_warrior_name": "Guerreiro do ano",
"achievement_year_warrior_desc": "Faça check-in por 365 dias consecutivos",
"total": "Total",
"status": "Status",
"pointsBreakdown": "Detalhamento de pontos",
"focusTimePoints": "Tempo de foco",
"focusTimePointsDesc": "1 ponto por minuto de foco",
"honestyBonusLabel": "Bônus de honestidade",
"honestyBonusDesc": "Pontos extras por registrar distrações",
"checkInPoints": "Check-in diário",
"checkInPointsDesc": "Pontos base para check-in diário",
"streakBonus": "Bônus de sequência",
"streakBonusDesc": "{days} check-ins consecutivos",
"achievementBonusLabel": "Bônus de conquista",
"weekdayS": "D",
"weekdayM": "S",
"weekdayT": "T",
"weekdayW": "Q",
"weekdayTh": "Q",
"weekdayF": "S",
"weekdaySa": "S"
} }

View File

@@ -121,5 +121,84 @@
"hindi": "हिन्दी", "hindi": "हिन्दी",
"indonesian": "Bahasa Indonesia", "indonesian": "Bahasa Indonesia",
"italian": "Italiano", "italian": "Italiano",
"arabic": "العربية" "arabic": "العربية",
"points": "Очки",
"level": "Уровень",
"checked": "Отмечено",
"checkIn": "Отметиться",
"earnedPoints": "Получено:",
"basePoints": "Базовые очки",
"honestyBonus": "Бонус за честность",
"totalPoints": "Всего очков: {count} ⚡",
"distractionsRecorded": "({count} {distractionText} записано)",
"achievementUnlocked": "🎖️ Достижение разблокировано!",
"bonusPoints": "+{points} очков ⚡",
"checkInSuccess": "Отметка успешна! +{points} очков ⚡",
"weeklyStreakBonus": "🎉 Бонус за недельную серию!",
"newAchievementUnlocked": "🎖️ Новое достижение разблокировано!",
"alreadyCheckedIn": "Вы уже отметились сегодня! Приходите завтра 📅",
"checkInCalendar": "Календарь отметок 📅",
"checkInToday": "📅 Отметиться сегодня",
"checkedInToday": "✓ Отмечен сегодня",
"currentStreak": "🔥 Текущая серия",
"longestStreak": "🏆 Самая длинная серия",
"days": "дней",
"daysCount": "{count} дней",
"achievements": "Достижения 🎖️",
"viewAllAchievements": "Посмотреть все достижения",
"allAchievementsComingSoon": "Полный экран достижений скоро!",
"profile": "Профиль",
"focuser": "Сосредоточенный",
"pointsToNextLevel": "{points} очков до уровня {level}",
"achievement_first_session_name": "Новичок фокуса",
"achievement_first_session_desc": "Завершите первую сессию фокусировки",
"achievement_sessions_10_name": "Начало",
"achievement_sessions_10_desc": "Завершите 10 сессий фокусировки",
"achievement_sessions_50_name": "Энтузиаст фокуса",
"achievement_sessions_50_desc": "Завершите 50 сессий фокусировки",
"achievement_sessions_100_name": "Мастер фокуса",
"achievement_sessions_100_desc": "Завершите 100 сессий фокусировки",
"achievement_honest_bronze_name": "Честный трекер · Бронза",
"achievement_honest_bronze_desc": "Честно запишите 50 отвлечений",
"achievement_honest_silver_name": "Честный трекер · Серебро",
"achievement_honest_silver_desc": "Честно запишите 200 отвлечений",
"achievement_honest_gold_name": "Честный трекер · Золото",
"achievement_honest_gold_desc": "Честно запишите 500 отвлечений",
"achievement_marathon_name": "Марафонец",
"achievement_marathon_desc": "Накопите 10 часов времени фокусировки",
"achievement_century_name": "Клуб столетия",
"achievement_century_desc": "Накопите 100 часов времени фокусировки",
"achievement_master_name": "Гроссмейстер фокуса",
"achievement_master_desc": "Накопите 1000 часов времени фокусировки",
"achievement_persistence_star_name": "Звезда упорства",
"achievement_persistence_star_desc": "Отмечайтесь 7 дней подряд",
"achievement_monthly_habit_name": "Месячная привычка",
"achievement_monthly_habit_desc": "Отмечайтесь 30 дней подряд",
"achievement_centurion_name": "Центурион",
"achievement_centurion_desc": "Отмечайтесь 100 дней подряд",
"achievement_year_warrior_name": "Воин года",
"achievement_year_warrior_desc": "Отмечайтесь 365 дней подряд",
"total": "Всего",
"status": "Статус",
"pointsBreakdown": "Разбивка очков",
"focusTimePoints": "Время фокусировки",
"focusTimePointsDesc": "1 очко за минуту фокусировки",
"honestyBonusLabel": "Бонус за честность",
"honestyBonusDesc": "Дополнительные очки за запись отвлечений",
"checkInPoints": "Ежедневная отметка",
"checkInPointsDesc": "Базовые очки за ежедневную отметку",
"streakBonus": "Бонус за серию",
"streakBonusDesc": "{days} дней подряд",
"achievementBonusLabel": "Бонус за достижение",
"weekdayS": "В",
"weekdayM": "П",
"weekdayT": "В",
"weekdayW": "С",
"weekdayTh": "Ч",
"weekdayF": "П",
"weekdaySa": "С"
} }

View File

@@ -121,5 +121,84 @@
"hindi": "हिन्दी", "hindi": "हिन्दी",
"indonesian": "Bahasa Indonesia", "indonesian": "Bahasa Indonesia",
"italian": "Italiano", "italian": "Italiano",
"arabic": "العربية" "arabic": "العربية",
"points": "积分",
"level": "等级",
"checked": "已签到",
"checkIn": "签到",
"earnedPoints": "获得:",
"basePoints": "基础积分",
"honestyBonus": "诚实奖励",
"totalPoints": "总积分:{count} ⚡",
"distractionsRecorded": "({count} {distractionText} 已记录)",
"achievementUnlocked": "🎖️ 成就解锁!",
"bonusPoints": "+{points} 积分 ⚡",
"checkInSuccess": "签到成功!+{points} 积分 ⚡",
"weeklyStreakBonus": "🎉 连续签到一周奖励!",
"newAchievementUnlocked": "🎖️ 新成就解锁!",
"alreadyCheckedIn": "你今天已经签到过了!明天再来 📅",
"checkInCalendar": "签到日历 📅",
"checkInToday": "📅 今日签到",
"checkedInToday": "✓ 今日已签到",
"currentStreak": "🔥 当前连续",
"longestStreak": "🏆 最长连续",
"days": "天",
"daysCount": "{count} 天",
"achievements": "成就 🎖️",
"viewAllAchievements": "查看所有成就",
"allAchievementsComingSoon": "完整成就页面即将推出!",
"profile": "个人资料",
"focuser": "专注者",
"pointsToNextLevel": "距离等级 {level} 还需 {points} 积分",
"achievement_first_session_name": "专注新手",
"achievement_first_session_desc": "完成首个专注会话",
"achievement_sessions_10_name": "初露锋芒",
"achievement_sessions_10_desc": "完成 10 次专注会话",
"achievement_sessions_50_name": "专注达人",
"achievement_sessions_50_desc": "完成 50 次专注会话",
"achievement_sessions_100_name": "专注大师",
"achievement_sessions_100_desc": "完成 100 次专注会话",
"achievement_honest_bronze_name": "诚实记录者·铜",
"achievement_honest_bronze_desc": "诚实记录 50 次分心",
"achievement_honest_silver_name": "诚实记录者·银",
"achievement_honest_silver_desc": "诚实记录 200 次分心",
"achievement_honest_gold_name": "诚实记录者·金",
"achievement_honest_gold_desc": "诚实记录 500 次分心",
"achievement_marathon_name": "马拉松跑者",
"achievement_marathon_desc": "累计专注 10 小时",
"achievement_century_name": "百时俱乐部",
"achievement_century_desc": "累计专注 100 小时",
"achievement_master_name": "专注宗师",
"achievement_master_desc": "累计专注 1000 小时",
"achievement_persistence_star_name": "坚持之星",
"achievement_persistence_star_desc": "连续签到 7 天",
"achievement_monthly_habit_name": "月度习惯",
"achievement_monthly_habit_desc": "连续签到 30 天",
"achievement_centurion_name": "百日勇士",
"achievement_centurion_desc": "连续签到 100 天",
"achievement_year_warrior_name": "年度战士",
"achievement_year_warrior_desc": "连续签到 365 天",
"total": "总计",
"status": "状态",
"pointsBreakdown": "积分明细",
"focusTimePoints": "专注时长",
"focusTimePointsDesc": "每专注1分钟获得1积分",
"honestyBonusLabel": "诚实奖励",
"honestyBonusDesc": "记录分心情况获得额外积分",
"checkInPoints": "每日签到",
"checkInPointsDesc": "每日首次签到获得基础积分",
"streakBonus": "连续签到奖励",
"streakBonusDesc": "连续签到 {days} 天",
"achievementBonusLabel": "成就奖励",
"weekdayS": "日",
"weekdayM": "一",
"weekdayT": "二",
"weekdayW": "三",
"weekdayTh": "四",
"weekdayF": "五",
"weekdaySa": "六"
} }

View File

@@ -0,0 +1,190 @@
/// Achievement types for tracking progress
enum AchievementType {
sessionCount, // Total number of completed sessions
distractionCount, // Total number of recorded distractions
totalMinutes, // Total minutes of focus time
consecutiveDays, // Consecutive check-in days
}
/// Configuration for a single achievement
class AchievementConfig {
final String id;
final String nameKey; // Localization key for name
final String descKey; // Localization key for description
final String icon; // Emoji icon
final AchievementType type;
final int requiredValue;
final int bonusPoints; // Points awarded when unlocked
const AchievementConfig({
required this.id,
required this.nameKey,
required this.descKey,
required this.icon,
required this.type,
required this.requiredValue,
required this.bonusPoints,
});
/// All available achievements in the app
static List<AchievementConfig> get all => [
// First session
const AchievementConfig(
id: 'first_session',
nameKey: 'achievement_first_session_name',
descKey: 'achievement_first_session_desc',
icon: '🎖️',
type: AchievementType.sessionCount,
requiredValue: 1,
bonusPoints: 10,
),
// Session milestones
const AchievementConfig(
id: 'sessions_10',
nameKey: 'achievement_sessions_10_name',
descKey: 'achievement_sessions_10_desc',
icon: '',
type: AchievementType.sessionCount,
requiredValue: 10,
bonusPoints: 50,
),
const AchievementConfig(
id: 'sessions_50',
nameKey: 'achievement_sessions_50_name',
descKey: 'achievement_sessions_50_desc',
icon: '🌟',
type: AchievementType.sessionCount,
requiredValue: 50,
bonusPoints: 200,
),
const AchievementConfig(
id: 'sessions_100',
nameKey: 'achievement_sessions_100_name',
descKey: 'achievement_sessions_100_desc',
icon: '💫',
type: AchievementType.sessionCount,
requiredValue: 100,
bonusPoints: 500,
),
// Honesty tracking series (KEY INNOVATION)
const AchievementConfig(
id: 'honest_bronze',
nameKey: 'achievement_honest_bronze_name',
descKey: 'achievement_honest_bronze_desc',
icon: '🧠',
type: AchievementType.distractionCount,
requiredValue: 50,
bonusPoints: 50,
),
const AchievementConfig(
id: 'honest_silver',
nameKey: 'achievement_honest_silver_name',
descKey: 'achievement_honest_silver_desc',
icon: '🧠',
type: AchievementType.distractionCount,
requiredValue: 200,
bonusPoints: 100,
),
const AchievementConfig(
id: 'honest_gold',
nameKey: 'achievement_honest_gold_name',
descKey: 'achievement_honest_gold_desc',
icon: '🧠',
type: AchievementType.distractionCount,
requiredValue: 500,
bonusPoints: 300,
),
// Focus time milestones
const AchievementConfig(
id: 'focus_5h',
nameKey: 'achievement_focus_5h_name',
descKey: 'achievement_focus_5h_desc',
icon: '⏱️',
type: AchievementType.totalMinutes,
requiredValue: 300, // 5 hours
bonusPoints: 100,
),
const AchievementConfig(
id: 'focus_25h',
nameKey: 'achievement_focus_25h_name',
descKey: 'achievement_focus_25h_desc',
icon: '',
type: AchievementType.totalMinutes,
requiredValue: 1500, // 25 hours
bonusPoints: 300,
),
const AchievementConfig(
id: 'focus_100h',
nameKey: 'achievement_focus_100h_name',
descKey: 'achievement_focus_100h_desc',
icon: '👑',
type: AchievementType.totalMinutes,
requiredValue: 6000, // 100 hours
bonusPoints: 1000,
),
// Check-in streaks
const AchievementConfig(
id: 'streak_3',
nameKey: 'achievement_streak_3_name',
descKey: 'achievement_streak_3_desc',
icon: '🔥',
type: AchievementType.consecutiveDays,
requiredValue: 3,
bonusPoints: 20,
),
const AchievementConfig(
id: 'streak_7',
nameKey: 'achievement_streak_7_name',
descKey: 'achievement_streak_7_desc',
icon: '🔥',
type: AchievementType.consecutiveDays,
requiredValue: 7,
bonusPoints: 50,
),
const AchievementConfig(
id: 'streak_30',
nameKey: 'achievement_streak_30_name',
descKey: 'achievement_streak_30_desc',
icon: '🔥',
type: AchievementType.consecutiveDays,
requiredValue: 30,
bonusPoints: 200,
),
const AchievementConfig(
id: 'streak_100',
nameKey: 'achievement_streak_100_name',
descKey: 'achievement_streak_100_desc',
icon: '🔥',
type: AchievementType.consecutiveDays,
requiredValue: 100,
bonusPoints: 1000,
),
];
/// Get achievement by ID
static AchievementConfig? getById(String id) {
try {
return all.firstWhere((achievement) => achievement.id == id);
} catch (e) {
return null;
}
}
/// Get all achievements of a specific type
static List<AchievementConfig> getByType(AchievementType type) {
return all.where((achievement) => achievement.type == type).toList();
}
}

View File

@@ -0,0 +1,144 @@
import 'package:hive/hive.dart';
part 'user_progress.g.dart';
@HiveType(typeId: 1)
class UserProgress extends HiveObject {
@HiveField(0)
int totalPoints;
@HiveField(1)
int currentPoints;
@HiveField(2)
DateTime? lastCheckInDate;
@HiveField(3)
int consecutiveCheckIns;
@HiveField(4)
Map<String, DateTime> unlockedAchievements;
@HiveField(5)
int totalFocusMinutes;
@HiveField(6)
int totalDistractions;
@HiveField(7)
int totalSessions;
@HiveField(8)
List<DateTime> checkInHistory;
UserProgress({
this.totalPoints = 0,
this.currentPoints = 0,
this.lastCheckInDate,
this.consecutiveCheckIns = 0,
Map<String, DateTime>? unlockedAchievements,
this.totalFocusMinutes = 0,
this.totalDistractions = 0,
this.totalSessions = 0,
List<DateTime>? checkInHistory,
}) : unlockedAchievements = unlockedAchievements ?? {},
checkInHistory = checkInHistory ?? [];
/// Get current level based on total points
int get level {
return LevelSystem.getLevel(totalPoints);
}
/// Get progress to next level (0.0 - 1.0)
double get levelProgress {
return LevelSystem.getLevelProgress(totalPoints);
}
/// Get points needed to reach next level
int get pointsToNextLevel {
return LevelSystem.getPointsToNextLevel(totalPoints);
}
/// Check if checked in today
bool get hasCheckedInToday {
if (lastCheckInDate == null) return false;
final now = DateTime.now();
return lastCheckInDate!.year == now.year &&
lastCheckInDate!.month == now.month &&
lastCheckInDate!.day == now.day;
}
/// Get longest check-in streak from history
int get longestCheckInStreak {
if (checkInHistory.isEmpty) return 0;
int maxStreak = 1;
int currentStreak = 1;
// Sort dates
final sortedDates = List<DateTime>.from(checkInHistory)
..sort((a, b) => a.compareTo(b));
for (int i = 1; i < sortedDates.length; i++) {
final diff = sortedDates[i].difference(sortedDates[i - 1]).inDays;
if (diff == 1) {
currentStreak++;
maxStreak = currentStreak > maxStreak ? currentStreak : maxStreak;
} else {
currentStreak = 1;
}
}
return maxStreak;
}
}
/// Level system configuration
class LevelSystem {
static const List<int> levelThresholds = [
0, // Level 0 → 1: 0 points
50, // Level 1 → 2: 50 points
150, // Level 2 → 3: 150 points
300, // Level 3 → 4: 300 points
500, // Level 4 → 5: 500 points
800, // Level 5 → 6: 800 points
1200, // Level 6 → 7: 1200 points
1800, // Level 7 → 8: 1800 points
2500, // Level 8 → 9: 2500 points
3500, // Level 9 → 10: 3500 points
];
static int getLevel(int points) {
for (int i = levelThresholds.length - 1; i >= 0; i--) {
if (points >= levelThresholds[i]) {
return i;
}
}
return 0;
}
static double getLevelProgress(int points) {
int currentLevel = getLevel(points);
if (currentLevel >= levelThresholds.length - 1) return 1.0;
int currentThreshold = levelThresholds[currentLevel];
int nextThreshold = levelThresholds[currentLevel + 1];
return (points - currentThreshold) / (nextThreshold - currentThreshold);
}
static int getPointsToNextLevel(int points) {
int currentLevel = getLevel(points);
if (currentLevel >= levelThresholds.length - 1) return 0;
return levelThresholds[currentLevel + 1] - points;
}
static int getNextLevelThreshold(int points) {
int currentLevel = getLevel(points);
if (currentLevel >= levelThresholds.length - 1) {
return levelThresholds.last;
}
return levelThresholds[currentLevel + 1];
}
}

View File

@@ -0,0 +1,65 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'user_progress.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class UserProgressAdapter extends TypeAdapter<UserProgress> {
@override
final int typeId = 1;
@override
UserProgress read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return UserProgress(
totalPoints: fields[0] as int,
currentPoints: fields[1] as int,
lastCheckInDate: fields[2] as DateTime?,
consecutiveCheckIns: fields[3] as int,
unlockedAchievements: (fields[4] as Map?)?.cast<String, DateTime>(),
totalFocusMinutes: fields[5] as int,
totalDistractions: fields[6] as int,
totalSessions: fields[7] as int,
checkInHistory: (fields[8] as List?)?.cast<DateTime>(),
);
}
@override
void write(BinaryWriter writer, UserProgress obj) {
writer
..writeByte(9)
..writeByte(0)
..write(obj.totalPoints)
..writeByte(1)
..write(obj.currentPoints)
..writeByte(2)
..write(obj.lastCheckInDate)
..writeByte(3)
..write(obj.consecutiveCheckIns)
..writeByte(4)
..write(obj.unlockedAchievements)
..writeByte(5)
..write(obj.totalFocusMinutes)
..writeByte(6)
..write(obj.totalDistractions)
..writeByte(7)
..write(obj.totalSessions)
..writeByte(8)
..write(obj.checkInHistory);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is UserProgressAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

@@ -4,6 +4,7 @@ import '../theme/app_colors.dart';
import '../theme/app_text_styles.dart'; import '../theme/app_text_styles.dart';
import '../services/storage_service.dart'; import '../services/storage_service.dart';
import '../services/encouragement_service.dart'; import '../services/encouragement_service.dart';
import '../models/achievement_config.dart';
import 'home_screen.dart'; import 'home_screen.dart';
import 'history_screen.dart'; import 'history_screen.dart';
@@ -11,12 +12,22 @@ import 'history_screen.dart';
class CompleteScreen extends StatelessWidget { class CompleteScreen extends StatelessWidget {
final int focusedMinutes; final int focusedMinutes;
final int distractionCount; final int distractionCount;
final int pointsEarned;
final int basePoints;
final int honestyBonus;
final int totalPoints;
final List<String> newAchievements;
final EncouragementService encouragementService; final EncouragementService encouragementService;
const CompleteScreen({ const CompleteScreen({
super.key, super.key,
required this.focusedMinutes, required this.focusedMinutes,
required this.distractionCount, required this.distractionCount,
required this.pointsEarned,
required this.basePoints,
required this.honestyBonus,
required this.totalPoints,
this.newAchievements = const [],
required this.encouragementService, required this.encouragementService,
}); });
@@ -33,99 +44,423 @@ class CompleteScreen extends StatelessWidget {
body: SafeArea( body: SafeArea(
child: Padding( child: Padding(
padding: const EdgeInsets.all(24.0), padding: const EdgeInsets.all(24.0),
child: Column( child: SingleChildScrollView(
mainAxisAlignment: MainAxisAlignment.center, child: Column(
children: [ mainAxisAlignment: MainAxisAlignment.center,
// Success Icon children: [
const Text( const SizedBox(height: 40),
'',
style: TextStyle(fontSize: 64),
),
const SizedBox(height: 32), // You focused for X minutes with success icon - left-right layout
Row(
// You focused for X minutes mainAxisAlignment: MainAxisAlignment.center,
Text( crossAxisAlignment: CrossAxisAlignment.center,
l10n.youFocusedFor,
style: AppTextStyles.headline,
),
const SizedBox(height: 8),
Text(
l10n.minutesValue(focusedMinutes, l10n.minutes(focusedMinutes)),
style: AppTextStyles.largeNumber,
),
const SizedBox(height: 40),
// Stats Card
Container(
width: double.infinity,
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: AppColors.white,
borderRadius: BorderRadius.circular(16),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( // Star icon on the left
l10n.totalToday(todayTotal), const Text('', style: TextStyle(fontSize: 64)),
style: AppTextStyles.bodyText, const SizedBox(width: 20),
), // Text content on the right
const SizedBox(height: 12), Column(
Text( mainAxisAlignment: MainAxisAlignment.center,
l10n.distractionsCount(todayDistractions, l10n.times(todayDistractions)), crossAxisAlignment: CrossAxisAlignment.start,
style: AppTextStyles.bodyText, children: [
), Text(l10n.youFocusedFor, style: AppTextStyles.headline),
const SizedBox(height: 20), const SizedBox(height: 8),
Text( Text(
'"$encouragement"', l10n.minutesValue(
style: AppTextStyles.encouragementQuote, focusedMinutes,
l10n.minutes(focusedMinutes),
),
style: AppTextStyles.largeNumber,
),
],
), ),
], ],
), ),
),
const SizedBox(height: 40), const SizedBox(height: 32),
// Start Another Button // Points Earned Section
SizedBox( _buildPointsCard(context, l10n),
width: double.infinity,
child: ElevatedButton( const SizedBox(height: 16),
// Achievement Unlocked (if any)
if (newAchievements.isNotEmpty)
..._buildAchievementCards(context, l10n),
const SizedBox(height: 16),
// Stats Card
Container(
width: double.infinity,
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: AppColors.white,
borderRadius: BorderRadius.circular(16),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.totalToday(todayTotal),
style: AppTextStyles.bodyText,
),
const SizedBox(height: 12),
Text(
l10n.distractionsCount(
todayDistractions,
l10n.times(todayDistractions),
),
style: AppTextStyles.bodyText,
),
const SizedBox(height: 20),
Text(
'"$encouragement"',
style: AppTextStyles.encouragementQuote,
),
],
),
),
const SizedBox(height: 24),
// Total Points Display
Text(
l10n.totalPoints(totalPoints),
style: const TextStyle(
fontFamily: 'Nunito',
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColors.primary,
),
),
const SizedBox(height: 24),
// Start Another Button
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () {
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(
builder: (context) => HomeScreen(
encouragementService: encouragementService,
),
),
(route) => false,
);
},
child: Text(l10n.startAnother),
),
),
const SizedBox(height: 16),
// View Full Report - Navigate to History
TextButton(
onPressed: () { onPressed: () {
Navigator.pushAndRemoveUntil( Navigator.pushAndRemoveUntil(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => HomeScreen( builder: (context) => const HistoryScreen(),
encouragementService: encouragementService,
),
), ),
(route) => false, (route) => route.isFirst,
); );
}, },
child: Text(l10n.startAnother), child: Text(l10n.viewHistory),
), ),
),
const SizedBox(height: 16), const SizedBox(height: 40),
],
// View Full Report - Navigate to History ),
TextButton(
onPressed: () {
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(
builder: (context) => const HistoryScreen(),
),
(route) => route.isFirst, // Keep only the home screen in stack
);
},
child: Text(l10n.viewHistory),
),
],
), ),
), ),
), ),
); );
} }
/// Build points earned card
Widget _buildPointsCard(BuildContext context, AppLocalizations l10n) {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: AppColors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: AppColors.primary.withValues(alpha: 0.3),
width: 2,
),
),
child: Column(
children: [
// Main points display
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
l10n.earnedPoints,
style: TextStyle(
fontFamily: 'Nunito',
fontSize: 18,
color: AppColors.textSecondary,
),
),
const SizedBox(width: 8),
Text(
'+$pointsEarned',
style: const TextStyle(
fontFamily: 'Nunito',
fontSize: 28,
fontWeight: FontWeight.bold,
color: AppColors.primary,
),
),
const Text('', style: TextStyle(fontSize: 24)),
],
),
const SizedBox(height: 16),
Divider(
thickness: 1,
color: AppColors.textSecondary.withValues(alpha: 0.2),
),
const SizedBox(height: 12),
// Points breakdown
_buildPointRow(l10n.basePoints, '+$basePoints', AppColors.success),
if (honestyBonus > 0) ...[
const SizedBox(height: 8),
_buildPointRow(
l10n.honestyBonus,
'+$honestyBonus',
AppColors.success,
subtitle: l10n.distractionsRecorded(
distractionCount,
l10n.distractions(distractionCount),
),
),
],
],
),
);
}
/// Build a single point row in the breakdown
Widget _buildPointRow(
String label,
String points,
Color color, {
String? subtitle,
}) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: Column(
children: [
Row(
children: [
Text(
'├─ ',
style: TextStyle(
color: AppColors.textSecondary.withValues(alpha: 0.4),
fontFamily: 'Nunito',
),
),
Text(
label,
style: const TextStyle(
fontFamily: 'Nunito',
fontSize: 14,
color: AppColors.textSecondary,
),
),
const Spacer(),
Text(
points,
style: TextStyle(
fontFamily: 'Nunito',
fontSize: 16,
fontWeight: FontWeight.w600,
color: color,
),
),
],
),
if (subtitle != null)
Padding(
padding: const EdgeInsets.only(left: 24, top: 4),
child: Row(
children: [
Text(
subtitle,
style: TextStyle(
fontFamily: 'Nunito',
fontSize: 12,
color: AppColors.textSecondary.withValues(alpha: 0.7),
),
),
],
),
),
],
),
);
}
/// Build achievement unlocked cards
List<Widget> _buildAchievementCards(
BuildContext context,
AppLocalizations l10n,
) {
return newAchievements.map((achievementId) {
final achievement = AchievementConfig.getById(achievementId);
if (achievement == null) return const SizedBox.shrink();
return Container(
margin: const EdgeInsets.only(bottom: 16),
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFFFFD700), Color(0xFFFFC107)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.orange.withValues(alpha: 0.4),
blurRadius: 20,
offset: const Offset(0, 8),
),
],
),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(achievement.icon, style: const TextStyle(fontSize: 32)),
const SizedBox(width: 12),
Text(
l10n.achievementUnlocked,
style: const TextStyle(
fontFamily: 'Nunito',
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
],
),
const SizedBox(height: 12),
Text(
_getLocalizedAchievementName(l10n, achievement.nameKey),
style: const TextStyle(
fontFamily: 'Nunito',
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: 4),
Text(
_getLocalizedAchievementDesc(l10n, achievement.descKey),
style: const TextStyle(
fontFamily: 'Nunito',
fontSize: 14,
color: Colors.white,
),
textAlign: TextAlign.center,
),
if (achievement.bonusPoints > 0) ...[
const SizedBox(height: 8),
Text(
l10n.bonusPoints(achievement.bonusPoints),
style: const TextStyle(
fontFamily: 'Nunito',
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
],
],
),
);
}).toList();
}
/// Get localized achievement name by key
String _getLocalizedAchievementName(AppLocalizations l10n, String key) {
switch (key) {
case 'achievement_first_session_name':
return l10n.achievement_first_session_name;
case 'achievement_sessions_10_name':
return l10n.achievement_sessions_10_name;
case 'achievement_sessions_50_name':
return l10n.achievement_sessions_50_name;
case 'achievement_sessions_100_name':
return l10n.achievement_sessions_100_name;
case 'achievement_honest_bronze_name':
return l10n.achievement_honest_bronze_name;
case 'achievement_honest_silver_name':
return l10n.achievement_honest_silver_name;
case 'achievement_honest_gold_name':
return l10n.achievement_honest_gold_name;
case 'achievement_marathon_name':
return l10n.achievement_marathon_name;
case 'achievement_century_name':
return l10n.achievement_century_name;
case 'achievement_master_name':
return l10n.achievement_master_name;
case 'achievement_persistence_star_name':
return l10n.achievement_persistence_star_name;
case 'achievement_monthly_habit_name':
return l10n.achievement_monthly_habit_name;
case 'achievement_centurion_name':
return l10n.achievement_centurion_name;
case 'achievement_year_warrior_name':
return l10n.achievement_year_warrior_name;
default:
return key;
}
}
/// Get localized achievement description by key
String _getLocalizedAchievementDesc(AppLocalizations l10n, String key) {
switch (key) {
case 'achievement_first_session_desc':
return l10n.achievement_first_session_desc;
case 'achievement_sessions_10_desc':
return l10n.achievement_sessions_10_desc;
case 'achievement_sessions_50_desc':
return l10n.achievement_sessions_50_desc;
case 'achievement_sessions_100_desc':
return l10n.achievement_sessions_100_desc;
case 'achievement_honest_bronze_desc':
return l10n.achievement_honest_bronze_desc;
case 'achievement_honest_silver_desc':
return l10n.achievement_honest_silver_desc;
case 'achievement_honest_gold_desc':
return l10n.achievement_honest_gold_desc;
case 'achievement_marathon_desc':
return l10n.achievement_marathon_desc;
case 'achievement_century_desc':
return l10n.achievement_century_desc;
case 'achievement_master_desc':
return l10n.achievement_master_desc;
case 'achievement_persistence_star_desc':
return l10n.achievement_persistence_star_desc;
case 'achievement_monthly_habit_desc':
return l10n.achievement_monthly_habit_desc;
case 'achievement_centurion_desc':
return l10n.achievement_centurion_desc;
case 'achievement_year_warrior_desc':
return l10n.achievement_year_warrior_desc;
default:
return key;
}
}
} }

View File

@@ -9,6 +9,8 @@ import '../services/di.dart';
import '../services/storage_service.dart'; import '../services/storage_service.dart';
import '../services/encouragement_service.dart'; import '../services/encouragement_service.dart';
import '../services/notification_service.dart'; import '../services/notification_service.dart';
import '../services/points_service.dart';
import '../services/achievement_service.dart';
import '../components/timer_display.dart'; import '../components/timer_display.dart';
import '../components/distraction_button.dart'; import '../components/distraction_button.dart';
import '../components/control_buttons.dart'; import '../components/control_buttons.dart';
@@ -38,6 +40,8 @@ class _FocusScreenState extends State<FocusScreen> with WidgetsBindingObserver {
bool _isInBackground = false; bool _isInBackground = false;
final NotificationService _notificationService = getIt<NotificationService>(); final NotificationService _notificationService = getIt<NotificationService>();
final StorageService _storageService = getIt<StorageService>(); final StorageService _storageService = getIt<StorageService>();
final PointsService _pointsService = getIt<PointsService>();
final AchievementService _achievementService = getIt<AchievementService>();
@override @override
void initState() { void initState() {
@@ -85,7 +89,8 @@ class _FocusScreenState extends State<FocusScreen> with WidgetsBindingObserver {
final l10n = AppLocalizations.of(context)!; final l10n = AppLocalizations.of(context)!;
final minutes = _remainingSeconds ~/ 60; final minutes = _remainingSeconds ~/ 60;
final seconds = _remainingSeconds % 60; final seconds = _remainingSeconds % 60;
final timeStr = '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}'; final timeStr =
'${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
_notificationService.showOngoingFocusNotification( _notificationService.showOngoingFocusNotification(
remainingMinutes: minutes, remainingMinutes: minutes,
remainingSeconds: seconds, remainingSeconds: seconds,
@@ -107,7 +112,8 @@ class _FocusScreenState extends State<FocusScreen> with WidgetsBindingObserver {
final l10n = AppLocalizations.of(context)!; final l10n = AppLocalizations.of(context)!;
final minutes = _remainingSeconds ~/ 60; final minutes = _remainingSeconds ~/ 60;
final seconds = _remainingSeconds % 60; final seconds = _remainingSeconds % 60;
final timeStr = '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}'; final timeStr =
'${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
_notificationService.updateOngoingFocusNotification( _notificationService.updateOngoingFocusNotification(
remainingMinutes: minutes, remainingMinutes: minutes,
remainingSeconds: seconds, remainingSeconds: seconds,
@@ -130,7 +136,8 @@ class _FocusScreenState extends State<FocusScreen> with WidgetsBindingObserver {
// Cancel ongoing notification and show completion notification // Cancel ongoing notification and show completion notification
await _notificationService.cancelOngoingFocusNotification(); await _notificationService.cancelOngoingFocusNotification();
await _saveFocusSession(completed: true); // Calculate points and update user progress
final pointsData = await _saveFocusSession(completed: true);
if (!mounted) return; if (!mounted) return;
@@ -162,6 +169,11 @@ class _FocusScreenState extends State<FocusScreen> with WidgetsBindingObserver {
builder: (context) => CompleteScreen( builder: (context) => CompleteScreen(
focusedMinutes: widget.durationMinutes, focusedMinutes: widget.durationMinutes,
distractionCount: _distractions.length, distractionCount: _distractions.length,
pointsEarned: pointsData['pointsEarned']!,
basePoints: pointsData['basePoints']!,
honestyBonus: pointsData['honestyBonus']!,
totalPoints: pointsData['totalPoints']!,
newAchievements: pointsData['newAchievements'] as List<String>,
encouragementService: widget.encouragementService, encouragementService: widget.encouragementService,
), ),
), ),
@@ -183,8 +195,11 @@ class _FocusScreenState extends State<FocusScreen> with WidgetsBindingObserver {
void _stopEarly() { void _stopEarly() {
final l10n = AppLocalizations.of(context)!; final l10n = AppLocalizations.of(context)!;
final actualMinutes = ((widget.durationMinutes * 60 - _remainingSeconds) / 60).floor(); final actualMinutes =
final minuteText = actualMinutes == 1 ? l10n.minutes(1) : l10n.minutes(actualMinutes); ((widget.durationMinutes * 60 - _remainingSeconds) / 60).floor();
final minuteText = actualMinutes == 1
? l10n.minutes(1)
: l10n.minutes(actualMinutes);
showDialog( showDialog(
context: context, context: context,
@@ -200,21 +215,37 @@ class _FocusScreenState extends State<FocusScreen> with WidgetsBindingObserver {
child: Text(l10n.keepGoing), child: Text(l10n.keepGoing),
), ),
TextButton( TextButton(
onPressed: () { onPressed: () async {
Navigator.pop(context); // Close dialog // Close dialog immediately
Navigator.pop(context);
_timer.cancel(); _timer.cancel();
_saveFocusSession(completed: false);
Navigator.pushReplacement( // Calculate points and update user progress
context, final pointsData = await _saveFocusSession(completed: false);
MaterialPageRoute(
builder: (context) => CompleteScreen( // Create a new context for navigation
focusedMinutes: actualMinutes, if (mounted) {
distractionCount: _distractions.length, WidgetsBinding.instance.addPostFrameCallback((_) {
encouragementService: widget.encouragementService, if (mounted) {
), Navigator.pushReplacement(
), context,
); MaterialPageRoute(
builder: (context) => CompleteScreen(
focusedMinutes: actualMinutes,
distractionCount: _distractions.length,
pointsEarned: pointsData['pointsEarned']!,
basePoints: pointsData['basePoints']!,
honestyBonus: pointsData['honestyBonus']!,
totalPoints: pointsData['totalPoints']!,
newAchievements:
pointsData['newAchievements'] as List<String>,
encouragementService: widget.encouragementService,
),
),
);
}
});
}
}, },
child: Text(l10n.yesStop), child: Text(l10n.yesStop),
), ),
@@ -223,7 +254,9 @@ class _FocusScreenState extends State<FocusScreen> with WidgetsBindingObserver {
); );
} }
Future<void> _saveFocusSession({required bool completed}) async { Future<Map<String, dynamic>> _saveFocusSession({
required bool completed,
}) async {
try { try {
final actualMinutes = completed final actualMinutes = completed
? widget.durationMinutes ? widget.durationMinutes
@@ -238,9 +271,47 @@ class _FocusScreenState extends State<FocusScreen> with WidgetsBindingObserver {
distractionTypes: _distractions, distractionTypes: _distractions,
); );
// Save session
await _storageService.saveFocusSession(session); await _storageService.saveFocusSession(session);
// Calculate points
final pointsBreakdown = _pointsService.calculateSessionPoints(session);
// Update user progress
final progress = _storageService.getUserProgress();
// Add points (convert to int explicitly)
progress.totalPoints += (pointsBreakdown['total']! as num).toInt();
progress.currentPoints += (pointsBreakdown['total']! as num).toInt();
// Update statistics
progress.totalSessions += 1;
progress.totalFocusMinutes += actualMinutes;
progress.totalDistractions += _distractions.length;
final newAchievements = await _achievementService.checkAchievementsAsync(
progress,
);
// Save updated progress
await _storageService.saveUserProgress(progress);
return {
'pointsEarned': pointsBreakdown['total']!,
'basePoints': pointsBreakdown['basePoints']!,
'honestyBonus': pointsBreakdown['honestyBonus']!,
'totalPoints': progress.totalPoints,
'newAchievements': newAchievements,
};
} catch (e) { } catch (e) {
// Ignore save errors silently // Return default values on error
return {
'pointsEarned': 0,
'basePoints': 0,
'honestyBonus': 0,
'totalPoints': 0,
'newAchievements': <String>[],
};
} }
} }
@@ -249,7 +320,10 @@ class _FocusScreenState extends State<FocusScreen> with WidgetsBindingObserver {
// Map distraction types to translations // Map distraction types to translations
final distractionOptions = [ final distractionOptions = [
(type: DistractionType.phoneNotification, label: l10n.distractionPhoneNotification), (
type: DistractionType.phoneNotification,
label: l10n.distractionPhoneNotification,
),
(type: DistractionType.socialMedia, label: l10n.distractionSocialMedia), (type: DistractionType.socialMedia, label: l10n.distractionSocialMedia),
(type: DistractionType.thoughts, label: l10n.distractionThoughts), (type: DistractionType.thoughts, label: l10n.distractionThoughts),
(type: DistractionType.other, label: l10n.distractionOther), (type: DistractionType.other, label: l10n.distractionOther),
@@ -356,7 +430,11 @@ class _FocusScreenState extends State<FocusScreen> with WidgetsBindingObserver {
// Show distraction-specific encouragement toast // Show distraction-specific encouragement toast
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text(widget.encouragementService.getRandomMessage(EncouragementType.distraction)), content: Text(
widget.encouragementService.getRandomMessage(
EncouragementType.distraction,
),
),
duration: const Duration(seconds: 2), duration: const Duration(seconds: 2),
behavior: SnackBarBehavior.floating, behavior: SnackBarBehavior.floating,
), ),
@@ -371,45 +449,30 @@ class _FocusScreenState extends State<FocusScreen> with WidgetsBindingObserver {
backgroundColor: AppColors.background, backgroundColor: AppColors.background,
body: SafeArea( body: SafeArea(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
Expanded( // Timer Display Component
child: SingleChildScrollView( TimerDisplay(remainingSeconds: _remainingSeconds),
padding: const EdgeInsets.all(24.0),
child: Column(
children: [
SizedBox(
height: MediaQuery.of(context).size.height * 0.2,
),
// Timer Display Component const SizedBox(height: 80),
TimerDisplay(remainingSeconds: _remainingSeconds),
const SizedBox(height: 80), // "I got distracted" Button Component
DistractionButton(
onPressed: _showDistractionSheet,
buttonText: l10n.iGotDistracted,
),
// "I got distracted" Button Component const SizedBox(height: 16),
DistractionButton(
onPressed: _showDistractionSheet,
buttonText: l10n.iGotDistracted,
),
const SizedBox(height: 16), // Control Buttons Component
ControlButtons(
// Control Buttons Component isPaused: _isPaused,
ControlButtons( onTogglePause: _togglePause,
isPaused: _isPaused, onStopEarly: _stopEarly,
onTogglePause: _togglePause, pauseText: l10n.pause,
onStopEarly: _stopEarly, resumeText: l10n.resume,
pauseText: l10n.pause, stopText: l10n.stopSession,
resumeText: l10n.resume,
stopText: l10n.stopSession,
),
SizedBox(
height: MediaQuery.of(context).size.height * 0.2,
),
],
),
),
), ),
], ],
), ),

View File

@@ -1,10 +1,11 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../l10n/app_localizations.dart';
import '../theme/app_colors.dart'; import '../theme/app_colors.dart';
import '../theme/app_text_styles.dart'; import '../theme/app_text_styles.dart';
import '../models/focus_session.dart'; import '../models/focus_session.dart';
import '../services/storage_service.dart'; import '../services/storage_service.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import '../l10n/app_localizations.dart'; import 'session_detail_screen.dart';
/// History Screen - Shows past focus sessions /// History Screen - Shows past focus sessions
class HistoryScreen extends StatefulWidget { class HistoryScreen extends StatefulWidget {
@@ -81,10 +82,7 @@ class _HistoryScreenState extends State<HistoryScreen> {
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
const Text( const Text('📊', style: TextStyle(fontSize: 64)),
'📊',
style: TextStyle(fontSize: 64),
),
const SizedBox(height: 24), const SizedBox(height: 24),
Text( Text(
l10n.noFocusSessionsYet, l10n.noFocusSessionsYet,
@@ -100,7 +98,7 @@ class _HistoryScreenState extends State<HistoryScreen> {
const SizedBox(height: 32), const SizedBox(height: 32),
ElevatedButton( ElevatedButton(
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
child: const Text('Start Focusing'), child: Text(l10n.startFocusing),
), ),
], ],
), ),
@@ -108,7 +106,12 @@ class _HistoryScreenState extends State<HistoryScreen> {
); );
} }
Widget _buildTodaySummary(AppLocalizations l10n, int totalMins, int distractions, int sessions) { Widget _buildTodaySummary(
AppLocalizations l10n,
int totalMins,
int distractions,
int sessions,
) {
return Container( return Container(
padding: const EdgeInsets.all(24), padding: const EdgeInsets.all(24),
decoration: BoxDecoration( decoration: BoxDecoration(
@@ -155,13 +158,20 @@ class _HistoryScreenState extends State<HistoryScreen> {
Row( Row(
children: [ children: [
Expanded( Expanded(
child: _buildStat('Total', l10n.minutesValue(totalMins, l10n.minutes(totalMins)), '⏱️'), child: _buildStat(
l10n.total,
l10n.minutesValue(totalMins, l10n.minutes(totalMins)),
'⏱️',
),
), ),
const SizedBox(width: 16), const SizedBox(width: 16),
Expanded( Expanded(
child: _buildStat( child: _buildStat(
'Distractions', l10n.distractions(distractions),
l10n.distractionsCount(distractions, l10n.times(distractions)), l10n.distractionsCount(
distractions,
l10n.times(distractions),
),
'🤚', '🤚',
), ),
), ),
@@ -176,10 +186,7 @@ class _HistoryScreenState extends State<HistoryScreen> {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(emoji, style: const TextStyle(fontSize: 24)),
emoji,
style: const TextStyle(fontSize: 24),
),
const SizedBox(height: 8), const SizedBox(height: 8),
Text( Text(
label, label,
@@ -204,7 +211,11 @@ class _HistoryScreenState extends State<HistoryScreen> {
); );
} }
Widget _buildDateSection(AppLocalizations l10n, DateTime date, List<FocusSession> sessions) { Widget _buildDateSection(
AppLocalizations l10n,
DateTime date,
List<FocusSession> sessions,
) {
final isToday = _isToday(date); final isToday = _isToday(date);
final dateLabel = isToday final dateLabel = isToday
? l10n.today ? l10n.today
@@ -257,83 +268,113 @@ class _HistoryScreenState extends State<HistoryScreen> {
final statusEmoji = session.completed ? '' : '⏸️'; final statusEmoji = session.completed ? '' : '⏸️';
final statusText = session.completed ? l10n.completed : l10n.stoppedEarly; final statusText = session.completed ? l10n.completed : l10n.stoppedEarly;
return Container( return GestureDetector(
margin: const EdgeInsets.only(bottom: 12), onTap: () {
padding: const EdgeInsets.all(16), Navigator.push(
decoration: BoxDecoration( context,
color: AppColors.white, MaterialPageRoute(
borderRadius: BorderRadius.circular(12), builder: (context) => SessionDetailScreen(session: session),
border: Border.all( ),
color: AppColors.divider, );
width: 1, },
child: Container(
margin: const EdgeInsets.only(bottom: 16),
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: AppColors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: AppColors.divider, width: 1),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.05),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
), ),
), child: Row(
child: Row( children: [
children: [ // Time
// Time Text(
Text( timeStr,
timeStr, style: const TextStyle(
style: const TextStyle(
fontFamily: 'Nunito',
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColors.textPrimary,
),
),
const SizedBox(width: 16),
// Duration
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.minutesValue(session.actualMinutes, l10n.minutes(session.actualMinutes)),
style: AppTextStyles.bodyText,
),
if (session.distractionCount > 0)
Padding(
padding: const EdgeInsets.only(top: 4),
child: Text(
l10n.distractionsCount(session.distractionCount, l10n.times(session.distractionCount)),
style: const TextStyle(
fontFamily: 'Nunito',
fontSize: 14,
fontWeight: FontWeight.w400,
color: AppColors.textSecondary,
),
),
),
],
),
),
// Status badge
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: session.completed
? AppColors.success.withValues(alpha: 0.1)
: AppColors.distractionButton,
borderRadius: BorderRadius.circular(8),
),
child: Text(
'$statusEmoji $statusText',
style: TextStyle(
fontFamily: 'Nunito', fontFamily: 'Nunito',
fontSize: 12, fontSize: 18,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w700,
color: session.completed color: AppColors.textPrimary,
? AppColors.success
: AppColors.textSecondary,
), ),
), ),
),
], const SizedBox(width: 20),
// Duration
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.minutesValue(
session.actualMinutes,
l10n.minutes(session.actualMinutes),
),
style: const TextStyle(
fontFamily: 'Nunito',
fontSize: 18,
fontWeight: FontWeight.w600,
color: AppColors.textPrimary,
),
),
if (session.distractionCount > 0)
Padding(
padding: const EdgeInsets.only(top: 6),
child: Text(
l10n.distractionsCount(
session.distractionCount,
l10n.times(session.distractionCount),
),
style: const TextStyle(
fontFamily: 'Nunito',
fontSize: 16,
fontWeight: FontWeight.w400,
color: AppColors.textSecondary,
),
),
),
],
),
),
// Status badge
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: session.completed
? AppColors.success.withValues(alpha: 0.1)
: AppColors.distractionButton,
borderRadius: BorderRadius.circular(12),
),
child: Text(
'$statusEmoji $statusText',
style: TextStyle(
fontFamily: 'Nunito',
fontSize: 14,
fontWeight: FontWeight.w600,
color: session.completed
? AppColors.success
: AppColors.textSecondary,
),
),
),
// Arrow indicator
const SizedBox(width: 12),
const Icon(
Icons.arrow_forward_ios,
size: 16,
color: AppColors.textSecondary,
),
],
),
), ),
); );
} }

View File

@@ -3,9 +3,12 @@ import '../l10n/app_localizations.dart';
import '../theme/app_colors.dart'; import '../theme/app_colors.dart';
import '../theme/app_text_styles.dart'; import '../theme/app_text_styles.dart';
import '../services/encouragement_service.dart'; import '../services/encouragement_service.dart';
import '../services/storage_service.dart';
import '../services/di.dart';
import 'focus_screen.dart'; import 'focus_screen.dart';
import 'history_screen.dart'; import 'history_screen.dart';
import 'settings_screen.dart'; import 'settings_screen.dart';
import 'profile_screen.dart';
/// Home Screen - Loads default duration from settings /// Home Screen - Loads default duration from settings
class HomeScreen extends StatefulWidget { class HomeScreen extends StatefulWidget {
@@ -22,6 +25,7 @@ class HomeScreen extends StatefulWidget {
class _HomeScreenState extends State<HomeScreen> { class _HomeScreenState extends State<HomeScreen> {
int _defaultDuration = 25; int _defaultDuration = 25;
final StorageService _storageService = getIt<StorageService>();
@override @override
void initState() { void initState() {
@@ -46,6 +50,7 @@ class _HomeScreenState extends State<HomeScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!; final l10n = AppLocalizations.of(context)!;
final progress = _storageService.getUserProgress();
return Scaffold( return Scaffold(
backgroundColor: AppColors.background, backgroundColor: AppColors.background,
@@ -53,8 +58,12 @@ class _HomeScreenState extends State<HomeScreen> {
child: Padding( child: Padding(
padding: const EdgeInsets.all(24.0), padding: const EdgeInsets.all(24.0),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
// Points Card at the top
_buildPointsCard(context, progress),
const SizedBox(height: 32),
// App Title // App Title
Text( Text(
l10n.appTitle, l10n.appTitle,
@@ -100,9 +109,10 @@ class _HomeScreenState extends State<HomeScreen> {
), ),
), ),
); );
// Reload duration when returning // Reload duration and refresh points when returning
if (result == true || mounted) { if (result == true || mounted) {
_loadDefaultDuration(); _loadDefaultDuration();
setState(() {}); // Refresh to show updated points
} }
}, },
child: Row( child: Row(
@@ -168,4 +178,156 @@ class _HomeScreenState extends State<HomeScreen> {
), ),
); );
} }
/// Build points card widget
Widget _buildPointsCard(BuildContext context, progress) {
final l10n = AppLocalizations.of(context)!;
return GestureDetector(
onTap: () async {
// Navigate to ProfileScreen
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const ProfileScreen(),
),
);
// Refresh points when returning from ProfileScreen
setState(() {});
},
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
AppColors.primary.withValues(alpha: 0.1),
AppColors.primary.withValues(alpha: 0.05),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: AppColors.primary.withValues(alpha: 0.2),
width: 1,
),
),
child: Row(
children: [
// Left side: Points and Level
Expanded(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
// Points
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Text(
'',
style: TextStyle(fontSize: 20),
),
const SizedBox(width: 4),
Text(
'${progress.totalPoints}',
style: const TextStyle(
fontFamily: 'Nunito',
fontSize: 24,
fontWeight: FontWeight.bold,
color: AppColors.primary,
),
),
],
),
Text(
l10n.points,
style: const TextStyle(
fontFamily: 'Nunito',
fontSize: 12,
color: AppColors.textSecondary,
),
),
],
),
// Level
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Text(
'🎖️',
style: TextStyle(fontSize: 20),
),
const SizedBox(width: 4),
Text(
'Lv ${progress.level}',
style: const TextStyle(
fontFamily: 'Nunito',
fontSize: 24,
fontWeight: FontWeight.bold,
color: AppColors.primary,
),
),
],
),
Text(
l10n.level,
style: const TextStyle(
fontFamily: 'Nunito',
fontSize: 12,
color: AppColors.textSecondary,
),
),
],
),
],
),
),
// Right side: Check-in status
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: progress.hasCheckedInToday
? AppColors.success.withValues(alpha: 0.1)
: AppColors.white.withValues(alpha: 0.5),
borderRadius: BorderRadius.circular(12),
),
child: Column(
children: [
Text(
progress.hasCheckedInToday ? '' : '📅',
style: const TextStyle(fontSize: 20),
),
const SizedBox(height: 4),
Text(
progress.hasCheckedInToday ? l10n.checked : l10n.checkIn,
style: TextStyle(
fontFamily: 'Nunito',
fontSize: 10,
color: progress.hasCheckedInToday
? AppColors.success
: AppColors.textSecondary,
fontWeight: FontWeight.w600,
),
),
if (progress.consecutiveCheckIns > 0)
Text(
'🔥 ${progress.consecutiveCheckIns}',
style: const TextStyle(
fontFamily: 'Nunito',
fontSize: 12,
fontWeight: FontWeight.bold,
color: AppColors.primary,
),
),
],
),
),
],
),
),
);
}
} }

View File

@@ -0,0 +1,830 @@
import 'package:flutter/material.dart';
import '../l10n/app_localizations.dart';
import '../theme/app_colors.dart';
import '../theme/app_constants.dart';
import '../services/storage_service.dart';
import '../services/points_service.dart';
import '../services/achievement_service.dart';
import '../services/di.dart';
import '../models/user_progress.dart';
import '../models/achievement_config.dart';
/// Profile Screen - Shows user points, level, check-in calendar, and achievements
class ProfileScreen extends StatefulWidget {
const ProfileScreen({super.key});
@override
State<ProfileScreen> createState() => _ProfileScreenState();
}
class _ProfileScreenState extends State<ProfileScreen> {
final StorageService _storageService = getIt<StorageService>();
final PointsService _pointsService = getIt<PointsService>();
final AchievementService _achievementService = getIt<AchievementService>();
late UserProgress _progress;
@override
void initState() {
super.initState();
_progress = _storageService.getUserProgress();
}
Future<void> _handleCheckIn() async {
final l10n = AppLocalizations.of(context)!;
if (_progress.hasCheckedInToday) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(l10n.alreadyCheckedIn),
duration: const Duration(seconds: 2),
),
);
return;
}
// Process check-in with detailed breakdown
final checkInResult = _pointsService.processCheckIn(_progress);
final pointsEarned = checkInResult['points'] as int;
// Add points
_progress.totalPoints += pointsEarned;
_progress.currentPoints += pointsEarned;
// Check for newly unlocked achievements (streak achievements) asynchronously
final newAchievements = await _achievementService.checkAchievementsAsync(
_progress,
);
// Save progress
await _storageService.saveUserProgress(_progress);
// Update UI
setState(() {});
// Show success message
if (!mounted) return;
String message = l10n.checkInSuccess(pointsEarned);
if (_progress.consecutiveCheckIns % 7 == 0) {
message += '\n${l10n.weeklyStreakBonus}';
}
if (newAchievements.isNotEmpty) {
message += '\n${l10n.newAchievementUnlocked}';
}
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
duration: const Duration(seconds: 3),
backgroundColor: AppColors.success,
),
);
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
backgroundColor: AppColors.background,
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
title: Text(
l10n.profile,
style: const TextStyle(
fontFamily: 'Nunito',
fontSize: 20,
fontWeight: FontWeight.bold,
color: AppColors.textPrimary,
),
),
centerTitle: true,
),
body: SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
// User header card
_buildUserHeaderCard(l10n),
const SizedBox(height: 24),
// Check-in calendar
_buildCheckInCalendar(l10n),
const SizedBox(height: 24),
// Achievement wall
_buildAchievementWall(l10n),
const SizedBox(height: 24),
],
),
),
),
);
}
/// Build user header card with points, level, and progress bar
Widget _buildUserHeaderCard(AppLocalizations l10n) {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [AppColors.primary, AppColors.primary.withValues(alpha: 0.8)],
),
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: AppColors.primary.withValues(alpha: 0.3),
blurRadius: 12,
offset: const Offset(0, 4),
),
],
),
child: Column(
children: [
// User icon (placeholder)
const CircleAvatar(
radius: 40,
backgroundColor: Colors.white,
child: Text('👤', style: TextStyle(fontSize: 40)),
),
const SizedBox(height: 16),
// User name (placeholder)
Text(
l10n.focuser,
style: const TextStyle(
fontFamily: 'Nunito',
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: 20),
// Points and Level row
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
// Points
Column(
children: [
Row(
children: [
const Text('', style: TextStyle(fontSize: 28)),
const SizedBox(width: 4),
Text(
'${_progress.totalPoints}',
style: const TextStyle(
fontFamily: 'Nunito',
fontSize: 28,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
],
),
Text(
l10n.points,
style: const TextStyle(
fontFamily: 'Nunito',
fontSize: 14,
color: Colors.white,
),
),
],
),
// Divider
Container(
height: 40,
width: 1,
color: Colors.white.withValues(alpha: 0.3),
),
// Level
Column(
children: [
Container(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 4,
),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(16),
),
child: Row(
children: [
const Text('🎖️', style: TextStyle(fontSize: 24)),
const SizedBox(width: 4),
Text(
'Lv ${_progress.level}',
style: const TextStyle(
fontFamily: 'Nunito',
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
],
),
),
const SizedBox(height: 4),
Text(
l10n.level,
style: const TextStyle(
fontFamily: 'Nunito',
fontSize: 14,
color: Colors.white,
),
),
],
),
],
),
const SizedBox(height: 20),
// Level progress bar
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.pointsToNextLevel(
_progress.pointsToNextLevel,
_progress.level + 1,
),
style: TextStyle(
fontFamily: 'Nunito',
fontSize: 12,
color: Colors.white.withValues(alpha: 0.9),
),
),
const SizedBox(height: 8),
Stack(
children: [
// Background bar
Container(
height: 10,
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(5),
),
),
// Progress bar
FractionallySizedBox(
widthFactor: _progress.levelProgress,
child: Container(
height: 10,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(5),
),
),
),
],
),
const SizedBox(height: 4),
Align(
alignment: Alignment.centerRight,
child: Text(
'${(_progress.levelProgress * 100).toInt()}%',
style: TextStyle(
fontFamily: 'Nunito',
fontSize: 12,
color: Colors.white.withValues(alpha: 0.9),
),
),
),
],
),
],
),
);
}
/// Build check-in calendar section
Widget _buildCheckInCalendar(AppLocalizations l10n) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColors.white,
borderRadius: BorderRadius.circular(16),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
l10n.checkInCalendar,
style: const TextStyle(
fontFamily: 'Nunito',
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppColors.textPrimary,
),
),
Text(
l10n.daysCount(_progress.checkInHistory.length),
style: const TextStyle(
fontFamily: 'Nunito',
fontSize: 14,
color: AppColors.textSecondary,
),
),
],
),
const SizedBox(height: 16),
// Check-in button
if (!_progress.hasCheckedInToday)
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _handleCheckIn,
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primary,
padding: const EdgeInsets.symmetric(vertical: 12),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
l10n.checkInToday,
style: const TextStyle(
fontFamily: 'Nunito',
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
],
),
),
),
if (_progress.hasCheckedInToday)
Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 12),
decoration: BoxDecoration(
color: AppColors.success.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
l10n.checkedInToday,
style: const TextStyle(
fontFamily: 'Nunito',
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColors.success,
),
),
],
),
),
const SizedBox(height: 16),
// Stats row
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildStatItem(
l10n.currentStreak,
l10n.daysCount(_progress.consecutiveCheckIns),
),
Container(height: 40, width: 1, color: AppColors.divider),
_buildStatItem(
l10n.longestStreak,
l10n.daysCount(_progress.longestCheckInStreak),
),
],
),
const SizedBox(height: 16),
// Calendar grid (last 28 days)
_buildCalendarGrid(l10n),
],
),
);
}
/// Build calendar grid showing check-in history
Widget _buildCalendarGrid(AppLocalizations l10n) {
final now = DateTime.now();
final today = DateTime(now.year, now.month, now.day);
return Column(
children: [
// Weekday labels
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
l10n.weekdayS,
l10n.weekdayM,
l10n.weekdayT,
l10n.weekdayW,
l10n.weekdayTh,
l10n.weekdayF,
l10n.weekdaySa,
]
.map(
(day) => SizedBox(
width: ProfileConstants.calendarCellSize,
child: Center(
child: Text(
day,
style: TextStyle(
fontFamily: 'Nunito',
fontSize: FontSizes.caption,
fontWeight: FontWeight.w600,
color: AppColors.textSecondary,
),
),
),
),
)
.toList(),
),
const SizedBox(height: 8),
// Calendar days (last 4 weeks)
Wrap(
spacing: 4,
runSpacing: 4,
children: List.generate(28, (index) {
final date = today.subtract(Duration(days: 27 - index));
final isCheckedIn = _progress.checkInHistory.any(
(checkInDate) =>
checkInDate.year == date.year &&
checkInDate.month == date.month &&
checkInDate.day == date.day,
);
final isToday = date == today;
return Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: isCheckedIn
? AppColors.primary.withValues(alpha: 0.2)
: Colors.transparent,
borderRadius: BorderRadius.circular(8),
border: isToday
? Border.all(color: AppColors.primary, width: 2)
: null,
),
child: Center(
child: Text(
isCheckedIn ? '' : date.day.toString(),
style: TextStyle(
fontFamily: 'Nunito',
fontSize: 14,
fontWeight: FontWeight.w600,
color: isCheckedIn
? AppColors.primary
: AppColors.textSecondary,
),
),
),
);
}),
),
],
);
}
/// Build a stat item
Widget _buildStatItem(String label, String value) {
return Column(
children: [
Text(
value,
style: const TextStyle(
fontFamily: 'Nunito',
fontSize: 20,
fontWeight: FontWeight.bold,
color: AppColors.primary,
),
),
const SizedBox(height: 4),
Text(
label,
style: const TextStyle(
fontFamily: 'Nunito',
fontSize: 12,
color: AppColors.textSecondary,
),
textAlign: TextAlign.center,
),
],
);
}
/// Build achievement wall section
Widget _buildAchievementWall(AppLocalizations l10n) {
final allAchievements = AchievementConfig.all;
final unlockedCount = _progress.unlockedAchievements.length;
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColors.white,
borderRadius: BorderRadius.circular(16),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
l10n.achievements,
style: const TextStyle(
fontFamily: 'Nunito',
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppColors.textPrimary,
),
),
Text(
'$unlockedCount/${allAchievements.length}',
style: const TextStyle(
fontFamily: 'Nunito',
fontSize: 14,
color: AppColors.textSecondary,
),
),
],
),
const SizedBox(height: 16),
// Achievement list
...allAchievements.take(6).map((achievement) {
final isUnlocked = _progress.unlockedAchievements.containsKey(
achievement.id,
);
final progress = _achievementService.getAchievementProgress(
_progress,
achievement,
);
final currentValue = _achievementService.getAchievementCurrentValue(
_progress,
achievement,
);
return _buildAchievementItem(
l10n: l10n,
achievement: achievement,
isUnlocked: isUnlocked,
progress: progress,
currentValue: currentValue,
);
}),
const SizedBox(height: 16),
// View all button
Center(
child: TextButton(
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(l10n.allAchievementsComingSoon),
duration: const Duration(seconds: 2),
),
);
},
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
l10n.viewAllAchievements,
style: const TextStyle(
fontFamily: 'Nunito',
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
const SizedBox(width: 4),
const Icon(Icons.arrow_forward, size: 16),
],
),
),
),
],
),
);
}
/// Build a single achievement item
Widget _buildAchievementItem({
required AppLocalizations l10n,
required AchievementConfig achievement,
required bool isUnlocked,
required double progress,
required int currentValue,
}) {
return Container(
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: isUnlocked
? AppColors.success.withValues(alpha: 0.05)
: AppColors.background,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: isUnlocked
? AppColors.success.withValues(alpha: 0.3)
: AppColors.divider,
width: 1,
),
),
child: Row(
children: [
// Icon
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: isUnlocked
? AppColors.success.withValues(alpha: 0.1)
: Colors.grey.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12),
),
child: Center(
child: Text(
achievement.icon,
style: TextStyle(
fontSize: 24,
color: isUnlocked ? null : Colors.grey,
),
),
),
),
const SizedBox(width: 12),
// Content
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_getLocalizedAchievementName(l10n, achievement.nameKey),
style: TextStyle(
fontFamily: 'Nunito',
fontSize: 14,
fontWeight: FontWeight.w600,
color: isUnlocked
? AppColors.textPrimary
: AppColors.textSecondary,
),
),
const SizedBox(height: 4),
Text(
_getLocalizedAchievementDesc(l10n, achievement.descKey),
style: TextStyle(
fontFamily: 'Nunito',
fontSize: 12,
color: AppColors.textSecondary.withValues(alpha: 0.8),
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
if (!isUnlocked) ...[
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: LinearProgressIndicator(
value: progress,
backgroundColor: Colors.grey.withValues(alpha: 0.2),
valueColor: const AlwaysStoppedAnimation(
AppColors.primary,
),
),
),
const SizedBox(width: 8),
Text(
'$currentValue/${achievement.requiredValue}',
style: const TextStyle(
fontFamily: 'Nunito',
fontSize: 10,
color: AppColors.textSecondary,
),
),
],
),
],
],
),
),
const SizedBox(width: 8),
// Status icon
if (isUnlocked)
const Icon(Icons.check_circle, color: AppColors.success, size: 24)
else
const Icon(
Icons.lock_outline,
color: AppColors.textSecondary,
size: 24,
),
],
),
);
}
/// Get localized achievement name by key
String _getLocalizedAchievementName(AppLocalizations l10n, String key) {
switch (key) {
case 'achievement_first_session_name':
return l10n.achievement_first_session_name;
case 'achievement_sessions_10_name':
return l10n.achievement_sessions_10_name;
case 'achievement_sessions_50_name':
return l10n.achievement_sessions_50_name;
case 'achievement_sessions_100_name':
return l10n.achievement_sessions_100_name;
case 'achievement_honest_bronze_name':
return l10n.achievement_honest_bronze_name;
case 'achievement_honest_silver_name':
return l10n.achievement_honest_silver_name;
case 'achievement_honest_gold_name':
return l10n.achievement_honest_gold_name;
case 'achievement_marathon_name':
return l10n.achievement_marathon_name;
case 'achievement_century_name':
return l10n.achievement_century_name;
case 'achievement_master_name':
return l10n.achievement_master_name;
case 'achievement_persistence_star_name':
return l10n.achievement_persistence_star_name;
case 'achievement_monthly_habit_name':
return l10n.achievement_monthly_habit_name;
case 'achievement_centurion_name':
return l10n.achievement_centurion_name;
case 'achievement_year_warrior_name':
return l10n.achievement_year_warrior_name;
default:
return key;
}
}
/// Get localized achievement description by key
String _getLocalizedAchievementDesc(AppLocalizations l10n, String key) {
switch (key) {
case 'achievement_first_session_desc':
return l10n.achievement_first_session_desc;
case 'achievement_sessions_10_desc':
return l10n.achievement_sessions_10_desc;
case 'achievement_sessions_50_desc':
return l10n.achievement_sessions_50_desc;
case 'achievement_sessions_100_desc':
return l10n.achievement_sessions_100_desc;
case 'achievement_honest_bronze_desc':
return l10n.achievement_honest_bronze_desc;
case 'achievement_honest_silver_desc':
return l10n.achievement_honest_silver_desc;
case 'achievement_honest_gold_desc':
return l10n.achievement_honest_gold_desc;
case 'achievement_marathon_desc':
return l10n.achievement_marathon_desc;
case 'achievement_century_desc':
return l10n.achievement_century_desc;
case 'achievement_master_desc':
return l10n.achievement_master_desc;
case 'achievement_persistence_star_desc':
return l10n.achievement_persistence_star_desc;
case 'achievement_monthly_habit_desc':
return l10n.achievement_monthly_habit_desc;
case 'achievement_centurion_desc':
return l10n.achievement_centurion_desc;
case 'achievement_year_warrior_desc':
return l10n.achievement_year_warrior_desc;
default:
return key;
}
}
}

View File

@@ -0,0 +1,602 @@
import 'package:flutter/material.dart';
import '../l10n/app_localizations.dart';
import '../theme/app_colors.dart';
import '../theme/app_text_styles.dart';
import '../models/focus_session.dart';
import '../models/achievement_config.dart';
import '../services/points_service.dart';
import '../services/storage_service.dart';
import '../services/encouragement_service.dart';
import '../services/di.dart';
/// Session Detail Screen - Shows detailed information about a past focus session
class SessionDetailScreen extends StatelessWidget {
final FocusSession session;
const SessionDetailScreen({super.key, required this.session});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final pointsService = getIt<PointsService>();
final storageService = getIt<StorageService>();
final encouragementService = getIt<EncouragementService>();
// Calculate points for this session
final pointsBreakdown = pointsService.calculateSessionPoints(session);
final pointsEarned = pointsBreakdown['total'] as int;
final basePoints = pointsBreakdown['basePoints'] as int;
final honestyBonus = pointsBreakdown['honestyBonus'] as int;
// Get user progress to show total points
final progress = storageService.getUserProgress();
final encouragement = encouragementService.getRandomMessage();
// Find achievements that might have been unlocked during this session
final sessionAchievements = _findSessionAchievements(
session,
storageService,
);
return Scaffold(
backgroundColor: AppColors.background,
appBar: AppBar(
title: Text(l10n.history),
backgroundColor: AppColors.background,
),
body: SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Session Date and Time
_buildSessionHeader(context, l10n),
const SizedBox(height: 18),
// Focused Time Section
Text(l10n.youFocusedFor, style: AppTextStyles.headline),
const SizedBox(height: 8),
Text(
l10n.minutesValue(
session.actualMinutes,
l10n.minutes(session.actualMinutes),
),
style: AppTextStyles.largeNumber,
),
const SizedBox(height: 24),
// Points Earned Section
_buildPointsCard(
context,
l10n,
pointsEarned,
basePoints,
honestyBonus,
),
const SizedBox(height: 16),
// Session Stats Card
Container(
width: double.infinity,
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: AppColors.white,
borderRadius: BorderRadius.circular(16),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
//Text(l10n.history, style: AppTextStyles.headline),
const SizedBox(height: 16),
_buildStatRow(
icon: '⏱️',
label: l10n.defaultFocusDuration,
value: l10n.minutesValue(
session.durationMinutes,
l10n.minutes(session.durationMinutes),
),
),
const SizedBox(height: 12),
_buildStatRow(
icon: '',
label: l10n.youFocusedFor,
value: l10n.minutesValue(
session.actualMinutes,
l10n.minutes(session.actualMinutes),
),
),
const SizedBox(height: 12),
_buildStatRow(
icon: '🤚',
label: l10n.distractions(session.distractionCount),
value:
'${session.distractionCount} ${l10n.times(session.distractionCount)}',
),
const SizedBox(height: 12),
_buildStatRow(
icon: '🏁',
label: l10n.status,
value: session.completed
? l10n.completed
: l10n.stoppedEarly,
),
const SizedBox(height: 20),
Text(
'"$encouragement"',
style: AppTextStyles.encouragementQuote,
),
],
),
),
const SizedBox(height: 16),
// Achievements Unlocked Section
if (sessionAchievements.isNotEmpty)
..._buildAchievementCards(context, l10n, sessionAchievements),
const SizedBox(height: 24),
// Total Points Display
Text(
l10n.totalPoints(progress.totalPoints),
style: const TextStyle(
fontFamily: 'Nunito',
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColors.primary,
),
),
const SizedBox(height: 40),
],
),
),
),
);
}
/// Build session header with date and time
Widget _buildSessionHeader(BuildContext context, AppLocalizations l10n) {
final dateStr = session.startTime.toLocal().toString().split(' ')[0];
final timeStr = session.startTime
.toLocal()
.toString()
.split(' ')[1]
.substring(0, 5);
return Container(
width: double.infinity,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: AppColors.white,
borderRadius: BorderRadius.circular(12),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
dateStr,
style: const TextStyle(
fontFamily: 'Nunito',
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColors.textSecondary,
),
),
const SizedBox(width: 12),
const Text('', style: TextStyle(color: AppColors.textSecondary)),
const SizedBox(width: 12),
Text(
timeStr,
style: const TextStyle(
fontFamily: 'Nunito',
fontSize: 18,
fontWeight: FontWeight.w700,
color: AppColors.textPrimary,
),
),
],
),
);
}
/// Build points earned card
Widget _buildPointsCard(
BuildContext context,
AppLocalizations l10n,
int pointsEarned,
int basePoints,
int honestyBonus,
) {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: AppColors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: AppColors.primary.withValues(alpha: 0.3),
width: 2,
),
),
child: Column(
children: [
// Main points display
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
l10n.earnedPoints,
style: TextStyle(
fontFamily: 'Nunito',
fontSize: 18,
color: AppColors.textSecondary,
),
),
const SizedBox(width: 8),
Text(
'+$pointsEarned',
style: const TextStyle(
fontFamily: 'Nunito',
fontSize: 28,
fontWeight: FontWeight.bold,
color: AppColors.primary,
),
),
const Text('', style: TextStyle(fontSize: 24)),
],
),
const SizedBox(height: 16),
Divider(
thickness: 1,
color: AppColors.textSecondary.withValues(alpha: 0.2),
),
const SizedBox(height: 12),
// Points breakdown
_buildPointRow(l10n.basePoints, '+$basePoints', AppColors.success),
if (honestyBonus > 0) ...[
const SizedBox(height: 8),
_buildPointRow(
l10n.honestyBonus,
'+$honestyBonus',
AppColors.success,
subtitle: l10n.distractionsRecorded(
session.distractionCount,
l10n.distractions(session.distractionCount),
),
),
],
],
),
);
}
/// Build a single point row in the breakdown
Widget _buildPointRow(
String label,
String points,
Color color, {
String? subtitle,
}) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: Column(
children: [
Row(
children: [
Text(
'├─ ',
style: TextStyle(
color: AppColors.textSecondary.withValues(alpha: 0.4),
fontFamily: 'Nunito',
),
),
Text(
label,
style: const TextStyle(
fontFamily: 'Nunito',
fontSize: 14,
color: AppColors.textSecondary,
),
),
const Spacer(),
Text(
points,
style: TextStyle(
fontFamily: 'Nunito',
fontSize: 16,
fontWeight: FontWeight.w600,
color: color,
),
),
],
),
if (subtitle != null)
Padding(
padding: const EdgeInsets.only(left: 24, top: 4),
child: Row(
children: [
Text(
subtitle,
style: TextStyle(
fontFamily: 'Nunito',
fontSize: 12,
color: AppColors.textSecondary.withValues(alpha: 0.7),
),
),
],
),
),
],
),
);
}
/// Build a single stat row
Widget _buildStatRow({
required String icon,
required String label,
required String value,
}) {
return Row(
children: [
Text(icon, style: const TextStyle(fontSize: 20)),
const SizedBox(width: 12),
Expanded(
child: Text(
label,
style: AppTextStyles.bodyText,
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
),
const SizedBox(width: 8),
Text(
value,
style: const TextStyle(
fontFamily: 'Nunito',
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColors.textPrimary,
),
),
],
);
}
/// Find achievements that might have been unlocked during this session
List<AchievementConfig> _findSessionAchievements(
FocusSession session,
StorageService storageService,
) {
final allAchievements = AchievementConfig.all;
final unlockedAchievements = storageService
.getUserProgress()
.unlockedAchievements;
final sessionAchievements = <AchievementConfig>[];
// Get all sessions to determine the state before this one
final allSessions = storageService.getAllSessions();
final sessionIndex = allSessions.indexOf(session);
// Calculate stats before this session
int sessionsBefore = sessionIndex;
int distractionsBefore = allSessions
.sublist(0, sessionIndex)
.fold(0, (sum, s) => sum + s.distractionCount);
int minutesBefore = allSessions
.sublist(0, sessionIndex)
.fold(0, (sum, s) => sum + s.actualMinutes);
// Check which achievements might have been unlocked by this session
for (final achievement in allAchievements) {
// Skip if not unlocked
if (!unlockedAchievements.containsKey(achievement.id)) {
continue;
}
// Check if this session could have unlocked the achievement
bool unlockedByThisSession = false;
switch (achievement.type) {
case AchievementType.sessionCount:
unlockedByThisSession =
sessionsBefore < achievement.requiredValue &&
(sessionsBefore + 1) >= achievement.requiredValue;
break;
case AchievementType.distractionCount:
unlockedByThisSession =
distractionsBefore < achievement.requiredValue &&
(distractionsBefore + session.distractionCount) >=
achievement.requiredValue;
break;
case AchievementType.totalMinutes:
unlockedByThisSession =
minutesBefore < achievement.requiredValue &&
(minutesBefore + session.actualMinutes) >=
achievement.requiredValue;
break;
case AchievementType.consecutiveDays:
// Consecutive days are not directly related to a single session
// but rather to check-ins, so we'll skip this type
break;
}
if (unlockedByThisSession) {
sessionAchievements.add(achievement);
}
}
return sessionAchievements;
}
/// Build achievement cards for achievements unlocked in this session
List<Widget> _buildAchievementCards(
BuildContext context,
AppLocalizations l10n,
List<AchievementConfig> achievements,
) {
return [
Text(l10n.achievements, style: AppTextStyles.headline),
const SizedBox(height: 16),
...achievements.map((achievement) {
return Container(
margin: const EdgeInsets.only(bottom: 16),
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFFFFD700), Color(0xFFFFC107)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.orange.withValues(alpha: 0.4),
blurRadius: 20,
offset: const Offset(0, 8),
),
],
),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(achievement.icon, style: const TextStyle(fontSize: 32)),
const SizedBox(width: 12),
Text(
l10n.achievementUnlocked,
style: const TextStyle(
fontFamily: 'Nunito',
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
],
),
const SizedBox(height: 12),
Text(
_getLocalizedAchievementName(l10n, achievement.nameKey),
style: const TextStyle(
fontFamily: 'Nunito',
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: 4),
Text(
_getLocalizedAchievementDesc(l10n, achievement.descKey),
style: const TextStyle(
fontFamily: 'Nunito',
fontSize: 14,
color: Colors.white,
),
textAlign: TextAlign.center,
),
if (achievement.bonusPoints > 0)
Padding(
padding: const EdgeInsets.only(top: 8),
child: Text(
'+${achievement.bonusPoints} 积分',
style: const TextStyle(
fontFamily: 'Nunito',
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
),
],
),
);
}),
];
}
/// Get localized achievement name by key
String _getLocalizedAchievementName(AppLocalizations l10n, String key) {
switch (key) {
case 'achievement_first_session_name':
return l10n.achievement_first_session_name;
case 'achievement_sessions_10_name':
return l10n.achievement_sessions_10_name;
case 'achievement_sessions_50_name':
return l10n.achievement_sessions_50_name;
case 'achievement_sessions_100_name':
return l10n.achievement_sessions_100_name;
case 'achievement_honest_bronze_name':
return l10n.achievement_honest_bronze_name;
case 'achievement_honest_silver_name':
return l10n.achievement_honest_silver_name;
case 'achievement_honest_gold_name':
return l10n.achievement_honest_gold_name;
case 'achievement_marathon_name':
return l10n.achievement_marathon_name;
case 'achievement_century_name':
return l10n.achievement_century_name;
case 'achievement_master_name':
return l10n.achievement_master_name;
case 'achievement_persistence_star_name':
return l10n.achievement_persistence_star_name;
case 'achievement_monthly_habit_name':
return l10n.achievement_monthly_habit_name;
case 'achievement_centurion_name':
return l10n.achievement_centurion_name;
case 'achievement_year_warrior_name':
return l10n.achievement_year_warrior_name;
default:
return key;
}
}
/// Get localized achievement description by key
String _getLocalizedAchievementDesc(AppLocalizations l10n, String key) {
switch (key) {
case 'achievement_first_session_desc':
return l10n.achievement_first_session_desc;
case 'achievement_sessions_10_desc':
return l10n.achievement_sessions_10_desc;
case 'achievement_sessions_50_desc':
return l10n.achievement_sessions_50_desc;
case 'achievement_sessions_100_desc':
return l10n.achievement_sessions_100_desc;
case 'achievement_honest_bronze_desc':
return l10n.achievement_honest_bronze_desc;
case 'achievement_honest_silver_desc':
return l10n.achievement_honest_silver_desc;
case 'achievement_honest_gold_desc':
return l10n.achievement_honest_gold_desc;
case 'achievement_marathon_desc':
return l10n.achievement_marathon_desc;
case 'achievement_century_desc':
return l10n.achievement_century_desc;
case 'achievement_master_desc':
return l10n.achievement_master_desc;
case 'achievement_persistence_star_desc':
return l10n.achievement_persistence_star_desc;
case 'achievement_monthly_habit_desc':
return l10n.achievement_monthly_habit_desc;
case 'achievement_centurion_desc':
return l10n.achievement_centurion_desc;
case 'achievement_year_warrior_desc':
return l10n.achievement_year_warrior_desc;
default:
return key;
}
}
}

View File

@@ -0,0 +1,111 @@
import '../models/user_progress.dart';
import '../models/achievement_config.dart';
/// Service for managing achievements
class AchievementService {
/// Check for newly unlocked achievements asynchronously
/// Returns list of newly unlocked achievement IDs
Future<List<String>> checkAchievementsAsync(UserProgress progress) async {
List<String> newlyUnlocked = [];
for (var achievement in AchievementConfig.all) {
// Skip if already unlocked
if (progress.unlockedAchievements.containsKey(achievement.id)) {
continue;
}
// Check if requirement is met
bool unlocked = false;
switch (achievement.type) {
case AchievementType.sessionCount:
unlocked = progress.totalSessions >= achievement.requiredValue;
break;
case AchievementType.distractionCount:
unlocked = progress.totalDistractions >= achievement.requiredValue;
break;
case AchievementType.totalMinutes:
unlocked = progress.totalFocusMinutes >= achievement.requiredValue;
break;
case AchievementType.consecutiveDays:
unlocked = progress.consecutiveCheckIns >= achievement.requiredValue;
break;
}
if (unlocked) {
// Mark as unlocked with timestamp
progress.unlockedAchievements[achievement.id] = DateTime.now();
// Award bonus points
progress.totalPoints += achievement.bonusPoints;
progress.currentPoints += achievement.bonusPoints;
newlyUnlocked.add(achievement.id);
}
}
return newlyUnlocked;
}
/// Get progress towards a specific achievement (0.0 - 1.0)
double getAchievementProgress(
UserProgress progress,
AchievementConfig achievement,
) {
int currentValue = 0;
switch (achievement.type) {
case AchievementType.sessionCount:
currentValue = progress.totalSessions;
break;
case AchievementType.distractionCount:
currentValue = progress.totalDistractions;
break;
case AchievementType.totalMinutes:
currentValue = progress.totalFocusMinutes;
break;
case AchievementType.consecutiveDays:
currentValue = progress.consecutiveCheckIns;
break;
}
return (currentValue / achievement.requiredValue).clamp(0.0, 1.0);
}
/// Get current value for achievement type
int getAchievementCurrentValue(
UserProgress progress,
AchievementConfig achievement,
) {
switch (achievement.type) {
case AchievementType.sessionCount:
return progress.totalSessions;
case AchievementType.distractionCount:
return progress.totalDistractions;
case AchievementType.totalMinutes:
return progress.totalFocusMinutes;
case AchievementType.consecutiveDays:
return progress.consecutiveCheckIns;
}
}
/// Get all achievements with their current progress
Map<AchievementConfig, double> getAllAchievementsWithProgress(
UserProgress progress,
) {
final result = <AchievementConfig, double>{};
for (var achievement in AchievementConfig.all) {
result[achievement] = getAchievementProgress(progress, achievement);
}
return result;
}
/// Get newly unlocked achievements since last check
/// This can be used to show notifications for achievements unlocked in background
List<String> getNewlyUnlockedAchievements(
UserProgress progress,
Set<String> previouslyUnlocked,
) {
final currentlyUnlocked = progress.unlockedAchievements.keys.toSet();
return currentlyUnlocked.difference(previouslyUnlocked).toList();
}
}

View File

@@ -4,6 +4,8 @@ import 'package:flutter/foundation.dart';
import 'storage_service.dart'; import 'storage_service.dart';
import 'notification_service.dart'; import 'notification_service.dart';
import 'encouragement_service.dart'; import 'encouragement_service.dart';
import 'points_service.dart';
import 'achievement_service.dart';
/// GetIt instance for dependency injection /// GetIt instance for dependency injection
final getIt = GetIt.instance; final getIt = GetIt.instance;
@@ -31,6 +33,10 @@ Future<void> initializeDI() async {
return service; return service;
}); });
// Register synchronous services
getIt.registerSingleton<PointsService>(PointsService());
getIt.registerSingleton<AchievementService>(AchievementService());
// Wait for all services to be initialized // Wait for all services to be initialized
await getIt.allReady(); await getIt.allReady();

View File

@@ -0,0 +1,164 @@
import 'dart:math';
import '../models/focus_session.dart';
import '../models/user_progress.dart';
/// Service for calculating and managing points
class PointsService {
/// Calculate points earned from a focus session
/// Returns a map with breakdown: {basePoints, honestyBonus, total, breakdown}
/// Note: breakdown contains labelKey and descriptionKey for localization
Map<String, dynamic> calculateSessionPoints(FocusSession session) {
// Base points = actual minutes focused
int basePoints = session.actualMinutes;
// Honesty bonus: reward for recording distractions (with cap to prevent abuse)
int honestyBonus = _calculateHonestyBonus(
session.distractionCount,
session.actualMinutes,
);
int total = basePoints + honestyBonus;
// Detailed breakdown for UI display (using localization keys)
List<Map<String, dynamic>> breakdown = [
{
'labelKey': 'focusTimePoints',
'value': basePoints,
'descriptionKey': 'focusTimePointsDesc',
},
{
'labelKey': 'honestyBonusLabel',
'value': honestyBonus,
'descriptionKey': 'honestyBonusDesc',
},
];
return {
'basePoints': basePoints,
'honestyBonus': honestyBonus,
'total': total,
'breakdown': breakdown,
};
}
/// Calculate honesty bonus with anti-abuse cap
/// Strategy: Max 1 rewarded distraction per 10 minutes
int _calculateHonestyBonus(int distractionCount, int minutes) {
if (distractionCount == 0) return 0;
// Cap: 1 rewarded distraction per 10 minutes
// 15 min → max 2 distractions
// 25 min → max 3 distractions
// 45 min → max 5 distractions
int maxBonusDistraction = max(1, (minutes / 10).ceil());
int rewardedCount = min(distractionCount, maxBonusDistraction);
return rewardedCount; // 1 point per recorded distraction (up to cap)
}
/// Process daily check-in and return points earned with detailed breakdown
/// Note: breakdown contains labelKey and descriptionKey for localization
Map<String, dynamic> processCheckIn(UserProgress progress) {
final now = DateTime.now();
// Base check-in points
int points = 5;
List<Map<String, dynamic>> breakdown = [
{
'labelKey': 'checkInPoints',
'value': 5,
'descriptionKey': 'checkInPointsDesc',
},
];
// Update check-in streak
if (_isConsecutiveDay(progress.lastCheckInDate, now)) {
progress.consecutiveCheckIns++;
// Bonus for streak milestones
if (progress.consecutiveCheckIns % 7 == 0) {
int weeklyBonus = 30;
points += weeklyBonus;
breakdown.add({
'labelKey': 'streakBonus',
'value': weeklyBonus,
'descriptionKey': 'streakBonusDesc',
'descriptionParams': {'days': progress.consecutiveCheckIns},
});
} else if (progress.consecutiveCheckIns % 30 == 0) {
int monthlyBonus = 100;
points += monthlyBonus;
breakdown.add({
'labelKey': 'streakBonus',
'value': monthlyBonus,
'descriptionKey': 'streakBonusDesc',
'descriptionParams': {'days': progress.consecutiveCheckIns},
});
}
} else {
progress.consecutiveCheckIns = 1;
}
// Update last check-in date
progress.lastCheckInDate = now;
// Add to check-in history (store date only, not time)
final dateOnly = DateTime(now.year, now.month, now.day);
if (!progress.checkInHistory.any((date) =>
date.year == dateOnly.year &&
date.month == dateOnly.month &&
date.day == dateOnly.day)) {
progress.checkInHistory.add(dateOnly);
}
return {
'points': points,
'consecutiveDays': progress.consecutiveCheckIns,
'breakdown': breakdown,
};
}
/// Check if two dates are consecutive days
bool _isConsecutiveDay(DateTime? lastDate, DateTime currentDate) {
if (lastDate == null) return false;
final lastDateOnly = DateTime(lastDate.year, lastDate.month, lastDate.day);
final currentDateOnly =
DateTime(currentDate.year, currentDate.month, currentDate.day);
final diff = currentDateOnly.difference(lastDateOnly).inDays;
return diff == 1;
}
/// Calculate level based on total points
Map<String, dynamic> calculateLevel(int totalPoints) {
// Simple level calculation: each level requires 100 points
int level = (totalPoints / 100).floor() + 1;
int pointsForCurrentLevel = (level - 1) * 100;
int pointsForNextLevel = level * 100;
int pointsInCurrentLevel = totalPoints - pointsForCurrentLevel;
double progress = pointsInCurrentLevel / 100;
return {
'level': level,
'pointsForCurrentLevel': pointsForCurrentLevel,
'pointsForNextLevel': pointsForNextLevel,
'pointsInCurrentLevel': pointsInCurrentLevel,
'progress': progress,
};
}
/// Get points balance summary
Map<String, dynamic> getPointsSummary(UserProgress progress) {
final levelInfo = calculateLevel(progress.totalPoints);
return {
'currentPoints': progress.currentPoints,
'totalPoints': progress.totalPoints,
'level': levelInfo['level'],
'levelProgress': levelInfo['progress'],
'consecutiveCheckIns': progress.consecutiveCheckIns,
'totalCheckIns': progress.checkInHistory.length,
};
}
}

View File

@@ -3,6 +3,8 @@ import 'package:flutter/foundation.dart';
import 'storage_service.dart'; import 'storage_service.dart';
import 'notification_service.dart'; import 'notification_service.dart';
import 'encouragement_service.dart'; import 'encouragement_service.dart';
import 'points_service.dart';
import 'achievement_service.dart';
/// Service Locator - 统一管理所有服务实例 /// Service Locator - 统一管理所有服务实例
class ServiceLocator { class ServiceLocator {
@@ -13,6 +15,8 @@ class ServiceLocator {
late StorageService _storageService; late StorageService _storageService;
late NotificationService _notificationService; late NotificationService _notificationService;
late EncouragementService _encouragementService; late EncouragementService _encouragementService;
late PointsService _pointsService;
late AchievementService _achievementService;
bool _isInitialized = false; bool _isInitialized = false;
/// 初始化所有服务 /// 初始化所有服务
@@ -33,6 +37,12 @@ class ServiceLocator {
_encouragementService = EncouragementService(); _encouragementService = EncouragementService();
await _encouragementService.loadMessages(); await _encouragementService.loadMessages();
// 初始化积分服务
_pointsService = PointsService();
// 初始化成就服务
_achievementService = AchievementService();
_isInitialized = true; _isInitialized = true;
if (kDebugMode) { if (kDebugMode) {
print('ServiceLocator initialized successfully'); print('ServiceLocator initialized successfully');
@@ -63,6 +73,18 @@ class ServiceLocator {
return _encouragementService; return _encouragementService;
} }
/// 获取积分服务实例
PointsService get pointsService {
_checkInitialized();
return _pointsService;
}
/// 获取成就服务实例
AchievementService get achievementService {
_checkInitialized();
return _achievementService;
}
/// 检查服务是否已初始化 /// 检查服务是否已初始化
void _checkInitialized() { void _checkInitialized() {
if (!_isInitialized) { if (!_isInitialized) {

View File

@@ -1,17 +1,23 @@
import 'package:hive_flutter/hive_flutter.dart'; import 'package:hive_flutter/hive_flutter.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import '../models/focus_session.dart'; import '../models/focus_session.dart';
import '../models/user_progress.dart';
/// Service to manage local storage using Hive /// Service to manage local storage using Hive
class StorageService { class StorageService {
static const String _focusSessionBox = 'focus_sessions'; static const String _focusSessionBox = 'focus_sessions';
static const String _userProgressBox = 'user_progress';
static const String _progressKey = 'user_progress_key';
// Cache for today's sessions to improve performance // Cache for today's sessions to improve performance
List<FocusSession>? _todaySessionsCache; List<FocusSession>? _todaySessionsCache;
DateTime? _cacheDate; DateTime? _cacheDate;
// Cache for user progress
UserProgress? _userProgressCache;
/// Initialize Hive storage service /// Initialize Hive storage service
/// ///
/// This method initializes Hive, registers adapters, and opens the focus sessions box. /// This method initializes Hive, registers adapters, and opens the focus sessions box.
/// It should be called once during app initialization. /// It should be called once during app initialization.
Future<void> init() async { Future<void> init() async {
@@ -20,10 +26,12 @@ class StorageService {
// Register adapters // Register adapters
Hive.registerAdapter(FocusSessionAdapter()); Hive.registerAdapter(FocusSessionAdapter());
Hive.registerAdapter(UserProgressAdapter());
// Open boxes // Open boxes
await Hive.openBox<FocusSession>(_focusSessionBox); await Hive.openBox<FocusSession>(_focusSessionBox);
await Hive.openBox<UserProgress>(_userProgressBox);
if (kDebugMode) { if (kDebugMode) {
print('StorageService initialized successfully'); print('StorageService initialized successfully');
} }
@@ -37,13 +45,92 @@ class StorageService {
/// Get the focus sessions box /// Get the focus sessions box
Box<FocusSession> get _sessionsBox => Hive.box<FocusSession>(_focusSessionBox); Box<FocusSession> get _sessionsBox => Hive.box<FocusSession>(_focusSessionBox);
/// Get the user progress box
Box<UserProgress> get _progressBox => Hive.box<UserProgress>(_userProgressBox);
/// Invalidate the cache when data changes /// Invalidate the cache when data changes
void _invalidateCache() { void _invalidateCache() {
_todaySessionsCache = null; _todaySessionsCache = null;
_cacheDate = null; _cacheDate = null;
} }
/// Invalidate user progress cache
void _invalidateProgressCache() {
_userProgressCache = null;
}
// ==================== User Progress Methods ====================
/// Get user progress (creates new one if doesn't exist)
UserProgress getUserProgress() {
try {
// Return cached progress if available
if (_userProgressCache != null) {
return _userProgressCache!;
}
// Try to get from box
var progress = _progressBox.get(_progressKey);
// Create new progress if doesn't exist
if (progress == null) {
progress = UserProgress();
_progressBox.put(_progressKey, progress);
}
// Cache and return
_userProgressCache = progress;
return progress;
} catch (e) {
if (kDebugMode) {
print('Failed to get user progress: $e');
}
// Return new progress as fallback
return UserProgress();
}
}
/// Save user progress
Future<void> saveUserProgress(UserProgress progress) async {
try {
await _progressBox.put(_progressKey, progress);
_userProgressCache = progress; // Update cache
} catch (e) {
if (kDebugMode) {
print('Failed to save user progress: $e');
}
rethrow;
}
}
/// Update user progress with a function
Future<void> updateUserProgress(Function(UserProgress) updateFn) async {
try {
final progress = getUserProgress();
updateFn(progress);
await saveUserProgress(progress);
} catch (e) {
if (kDebugMode) {
print('Failed to update user progress: $e');
}
rethrow;
}
}
/// Clear user progress (for testing/reset)
Future<void> clearUserProgress() async {
try {
await _progressBox.delete(_progressKey);
_invalidateProgressCache();
} catch (e) {
if (kDebugMode) {
print('Failed to clear user progress: $e');
}
rethrow;
}
}
/// Save a focus session to local storage /// Save a focus session to local storage
/// ///
/// [session] - The focus session to save /// [session] - The focus session to save

View File

@@ -0,0 +1,169 @@
/// Design system constants for FocusBuddy app
///
/// This file contains all magic numbers extracted from the codebase
/// to ensure consistency and maintainability.
library;
/// Spacing constants following 8px grid system
class AppSpacing {
AppSpacing._();
static const double xs = 4.0;
static const double sm = 8.0;
static const double md = 12.0;
static const double base = 16.0;
static const double lg = 20.0;
static const double xl = 24.0;
static const double xxl = 32.0;
static const double xxxl = 40.0;
static const double huge = 48.0;
static const double massive = 60.0;
static const double gigantic = 80.0;
}
/// Duration-related constants
class AppDurations {
AppDurations._();
// Default focus session duration (minutes)
static const int defaultFocusDuration = 25;
// Available duration options (minutes)
static const List<int> availableDurations = [15, 25, 45];
// Timer tick interval
static const Duration timerTickInterval = Duration(seconds: 1);
// Seconds per minute (for conversions)
static const int secondsPerMinute = 60;
// Notification update interval when app is backgrounded
static const int notificationUpdateIntervalSeconds = 30;
// Animation durations
static const Duration pageTransition = Duration(milliseconds: 300);
// SnackBar display durations
static const Duration snackBarShort = Duration(seconds: 2);
static const Duration snackBarMedium = Duration(seconds: 3);
}
/// Onboarding screen constants
class OnboardingConstants {
OnboardingConstants._();
static const int totalPages = 3;
static const double horizontalPadding = 32.0;
static const double emojiSize = 80.0;
static const double indicatorWidth = 24.0;
static const double indicatorHeight = 4.0;
static const double indicatorActiveWidth = 8.0;
static const double indicatorActiveHeight = 8.0;
}
/// Settings screen constants
class SettingsConstants {
SettingsConstants._();
static const double iconSize = 16.0;
static const double sectionSpacing = 20.0;
static const double optionHeight = 12.0;
static const double optionSpacing = 2.0;
static const double radioButtonSize = 20.0;
static const double radioCheckIconSize = 12.0;
static const double badgeFontSize = 12.0;
}
/// Profile screen constants
class ProfileConstants {
ProfileConstants._();
static const double avatarRadius = 40.0;
static const double avatarEmojiSize = 40.0;
static const double statDividerHeight = 40.0;
static const double progressBarHeight = 10.0;
static const double progressBarRadius = 5.0;
static const double calendarCellSize = 40.0;
static const int calendarDisplayDays = 28;
static const int maxDisplayedAchievements = 6;
// Check-in milestones
static const int weeklyCheckInMilestone = 7;
}
/// Completion screen constants
class CompletionConstants {
CompletionConstants._();
static const double emojiSize = 64.0;
}
/// Calendar constants (shared between profile and session detail)
class CalendarConstants {
CalendarConstants._();
static const int displayDays = 28;
static const int daysOffset = 27; // displayDays - 1
}
/// Points and gamification constants
class GameConstants {
GameConstants._();
// Points calculation
static const int pointsPerFocusMinute = 1;
static const int honestyBonusMinutesPerDistraction = 10;
// Check-in rewards
static const int checkInBasePoints = 5;
static const int weeklyStreakDays = 7;
static const int weeklyStreakBonus = 30;
static const int monthlyStreakDays = 30;
static const int monthlyStreakBonus = 100;
// Level progression
static const int pointsPerLevel = 100;
}
/// Notification IDs
class NotificationIds {
NotificationIds._();
static const int complete = 0;
static const int reminder = 1;
static const int ongoing = 2;
}
/// Icon sizes
class IconSizes {
IconSizes._();
static const double small = 12.0;
static const double medium = 16.0;
static const double large = 20.0;
static const double extraLarge = 24.0;
}
/// Font sizes (complementing AppTextStyles)
class FontSizes {
FontSizes._();
static const double caption = 12.0;
static const double body = 14.0;
static const double bodyLarge = 16.0;
static const double subtitle = 18.0;
static const double title = 20.0;
static const double heading = 28.0;
static const double display = 32.0;
}
/// Border radius values
class BorderRadii {
BorderRadii._();
static const double small = 8.0;
static const double medium = 12.0;
static const double large = 16.0;
static const double extraLarge = 24.0;
static const double circular = 999.0;
}

View File

@@ -1,558 +0,0 @@
# FocusBuddy MVP 上线清单
**目标**: 4 周内完成可上线版本
**策略**: 最小可行 → 快速上线 → 迭代优化
**创建日期**: 2025年11月22日
---
## 一、MVP 功能精简建议 ⚠️
### 1.1 必须保留(核心价值)
| 功能 | 优先级 | 理由 |
|------|--------|------|
| ✅ 一键开始专注25分钟固定 | P0 | 降低选择成本 |
| ✅ "I got distracted" 按钮 | P0 | 核心差异化功能 |
| ✅ 4种分心分类 | P0 | 提供情感支持 |
| ✅ 鼓励文案反馈 | P0 | 体现"温柔"定位 |
| ✅ 简单完成统计 | P0 | 提供成就感 |
### 1.2 建议延后V1.1 迭代)
| 功能 | 延后理由 | 替代方案 |
|------|---------|---------|
| ⏸️ 时长滑动调整5-60分钟 | 增加开发复杂度 | 固定25分钟 + 设置页预设3个选项 |
| ⏸️ 白噪音播放 | 需要音频资源采购 + 测试 | V1.0 不实现,聚焦核心体验 |
| ⏸️ PDF 报告导出 | 复杂度高,用户需求待验证 | 先用截图分享替代 |
| ⏸️ 成就徽章动画 | 需要 Lottie 资源 | 简化为静态图标 + 文字 |
| ⏸️ 每周趋势图表 | 需要图表库 | 仅显示"今日总时长" |
### 1.3 MVP 最小功能集3个核心页面
**页面1: Home Screen**
- 大按钮: "Start Focusing (25 min)"
- 小字提示: "Tap 'I got distracted' anytime — no guilt."
- 底部导航: History | Settings
**页面2: Focus Screen**
- 倒计时: 24:37
- 按钮: "I got distracted" (弹出4选项)
- 按钮: "Pause" | "Stop"
**页面3: Complete Screen**
- 标题: "You focused for 24 minutes"
- 今日统计: "Total today: 47 mins | Distractions: 2"
- 鼓励语: 随机一条
- 按钮: "Start Another"
**附加页面(简化版):**
- History: 仅显示当天记录(列表)
- Settings: 默认时长选择 | 隐私政策链接 | 去广告按钮
---
## 二、技术实现优化建议
### 2.1 依赖包精简(减少集成风险)
**必须集成:**
```yaml
dependencies:
flutter: sdk
hive: ^2.2.3 # 本地存储
hive_flutter: ^1.1.0
flutter_local_notifications: ^17.0.0 # 计时完成通知
path_provider: ^2.1.0 # 存储路径
```
**暂缓集成V1.1:**
```yaml
# workmanager: ^0.5.2 # 后台任务MVP 不需要)
# lottie: ^3.0.0 # 动画(用静态图标替代)
# just_audio: ^0.9.36 # 音频(延后)
# pdf: ^3.10.0 # 报告导出(延后)
```
**广告延后到 V1.0.1:**
```yaml
# google_mobile_ads: ^4.0.0 # 先上架审核通过再加广告
```
### 2.2 数据结构简化
```dart
// 最小可行数据模型
@HiveType(typeId: 0)
class FocusSession {
@HiveField(0)
DateTime startTime;
@HiveField(1)
int durationMinutes; // 实际专注时长
@HiveField(2)
int distractionCount; // 分心次数(简化为计数)
@HiveField(3)
bool completed; // 是否完成
}
// V1.1 再扩展详细分心类型
```
### 2.3 动画简化策略
| 原设计 | MVP 简化方案 | 节省开发时间 |
|--------|-------------|-------------|
| Lottie 粒子背景 | 纯色背景 + CSS 渐变 | 1天 |
| 计时器呼吸动画 | 静态显示 | 0.5天 |
| 成就徽章弹出 | 简单文字卡片淡入 | 1天 |
| 底部弹窗拖拽 | 标准 showModalBottomSheet | 0.5天 |
**总计节省: 3天开发时间**
---
## 三、上线前必备清单
### 3.1 应用商店准备
#### iOS App Store
- [ ] **开发者账号** ($99/年,需提前注册)
- [ ] **App 图标** 1024×1024 (无透明通道,必须)
- [ ] **截图** 至少3张 (6.5" iPhone)
- 建议: Home页 | Focus页 | Complete页
- [ ] **隐私政策链接** (托管在 GitHub Pages)
- [ ] **应用描述** (英文150-200字)
- [ ] **关键词** (最多100字符)
- 建议: focus,timer,pomodoro,gentle,ADHD,productivity,neurodivergent
#### Google Play Store
- [ ] **开发者账号** ($25 一次性)
- [ ] **App 图标** 512×512
- [ ] **截图** 至少2张 + 1张横幅图 (可选)
- [ ] **隐私政策链接**
- [ ] **内容分级问卷** (选择 "Everyone")
- [ ] **短描述** (80字) + 完整描述 (4000字)
### 3.2 合规文档(必须完成)
**优先级 P0:**
- [x] [privacy-policy.md](privacy-policy.md) - 需填写开发者信息
- [ ] **Terms of Service** (服务条款) - 简单版即可
- [ ] **Support Email** (必须可用)
- 建议: focusbuddy.support@gmail.com
**模板待补充:**
- [ ] 应用商店描述文案(中英文)
- [ ] 关键词优化列表
- [ ] ASO 元数据表格
### 3.3 测试清单(上线前必测)
#### 功能测试
- [ ] 计时器倒计时准确(误差 < 1秒/分钟)
- [ ] "I got distracted" 不中断计时
- [ ] 数据持久化(关闭 App 重开数据仍在)
- [ ] 完成后统计正确(时长 + 分心次数)
- [ ] 暂停/恢复功能正常
#### 平台测试
- [ ] iOS 真机测试至少1台推荐 iPhone 12+
- [ ] Android 真机测试至少2台不同品牌
- [ ] 适配刘海屏/水滴屏
- [ ] 横竖屏切换不崩溃
#### 边界测试
- [ ] 计时到0秒时行为正常
- [ ] 快速点击按钮不崩溃
- [ ] 本地存储达到上限时处理建议保留最近100条
- [ ] 系统通知权限被拒绝时提示
#### 性能测试
- [ ] 内存占用 < 100MB
- [ ] 冷启动时间 < 2秒
- [ ] 电池消耗正常1小时专注 < 5%电量)
---
## 四、风险预警与应对
### 4.1 高风险项(可能导致延期)
| 风险 | 概率 | 影响 | 预防措施 |
|------|------|------|---------|
| **iOS 审核被拒** | 60% | 延期1-2周 | 提前研读 [App Store 审核指南](https://developer.apple.com/app-store/review/guidelines/),避免医疗声明 |
| **AdMob 账号被封** | 30% | 收入归零 | MVP 先不集成广告,等有用户再加 |
| **Flutter 版本兼容问题** | 40% | 延期3-5天 | 使用稳定版 Flutter 3.16+,依赖包固定版本 |
| **真机测试发现严重 Bug** | 50% | 延期1周 | 第2周即开始真机测试不要等到最后 |
### 4.2 应对策略
**Plan A (理想):** 4周完成上线
**Plan B (现实):** 5-6周完成预留缓冲
**Plan C (保底):** 先上架 Android审核更快iOS 延后
---
## 五、MVP 开发路线图(调整版)
### Week 1: 核心框架 + 基础 UI
**目标:** 能跑通主流程,无需完美
- Day 1-2: Flutter 环境搭建 + 项目初始化
- 创建项目结构(参考 [ui-design-spec.md](ui-design-spec.md:589-619)
- 集成 Hive + 配置主题色
- Day 3-4: Home 页 + Focus 页 UI
- 硬编码数据,先实现布局
- 按钮可点击,无实际逻辑
- Day 5-7: 计时器核心逻辑
- 倒计时功能(使用 `Timer.periodic`
- 暂停/恢复/停止
- **里程碑:** 能完整跑一次25分钟计时
### Week 2: 数据持久化 + 分心记录
**目标:** 数据能保存和读取
- Day 8-9: Hive 数据存储
- 定义 FocusSession 模型
- 保存到本地数据库
- Day 10-11: 分心按钮 + Bottom Sheet
- 4种分心类型选择
- 点击后显示鼓励文案Toast
- Day 12-14: Complete 页 + 统计逻辑
- 显示当次专注时长
- 计算今日总时长和分心次数
- **里程碑:** 能看到历史数据
### Week 3: 设置页 + 通知 + 真机测试
**目标:** 功能完整,开始测试
- Day 15-16: Settings 页面
- 3个预设时长选择15/25/45分钟
- 去广告按钮(占位,不实现)
- 隐私政策链接
- Day 17-18: 本地通知
- 计时完成时弹通知
- 处理权限请求
- Day 19-21: 真机测试 + Bug 修复
- iOS 和 Android 各测至少2轮
- 修复崩溃和明显 Bug
- **里程碑:** 可以交给朋友测试
### Week 4: 上架准备 + 提交审核
**目标:** 提交 App Store 和 Play Store
- Day 22-23: 应用图标 + 截图制作
- 设计工具: Figma / Canva
- 准备所有尺寸资源
- Day 24-25: 商店页面填写
- 撰写应用描述(参考竞品)
- 上传隐私政策到 GitHub Pages
- Day 26: iOS 提交审核
- 打包 IPA + 上传 App Store Connect
- 提交审核通常需要1-3天
- Day 27: Android 提交审核
- 打包 AAB + 上传 Google Play Console
- 提交审核通常需要1-7天
- Day 28: 缓冲时间
- 处理审核反馈
- 准备推广素材
---
## 六、产品设计补充建议
### 6.1 增加的必要功能
#### 1. Onboarding 引导页(首次启动)
**为什么需要:**
- 解释 "I got distracted" 按钮的独特价值
- 降低新用户困惑
**设计2-3页滑动:**
```
页面1:
标题: "Focus without guilt"
说明: "This app is different — it won't punish you for losing focus."
页面2:
标题: "Tap when you get distracted"
说明: "We'll gently remind you to come back. No shame, no stress."
页面3:
标题: "Track your progress"
说明: "See how you're improving, one session at a time."
[Get Started]
```
**实现:**
- 使用 `SharedPreferences` 存储是否首次启动
- 使用 `PageView` 实现滑动
- **开发时间:** 1天
#### 2. 空状态提示History 页无数据时)
**当前问题:** 首次使用时 History 是空的,用户不知道发生了什么
**建议设计:**
```
┌─────────────────────────────────┐
│ │
│ 📊 │
│ │
│ No focus sessions yet │
│ │
│ Start your first session │
│ to see your progress here! │
│ │
│ [Start Focusing] │
│ │
└─────────────────────────────────┘
```
#### 3. 后台计时提醒
**当前问题:** 用户切到其他 App 可能忘记正在计时
**建议实现:**
- 进入后台时显示系统通知: "Focus session in progress — 12:34 remaining"
- 使用 `flutter_local_notifications` 持续更新
- **开发时间:** 0.5天
#### 4. 计时完成后的行为
**当前缺失:** 用户看到 Complete 页后,下一步做什么?
**建议增加:**
- "Take a 5-min break" 按钮(开始休息倒计时)
- "Start another session" 按钮(直接开始)
- "View history" 按钮(查看统计)
### 6.2 文案优化
#### 当前问题
部分文案过于书面,不够"温柔"
**建议修改:**
| 原文案 | 优化后 | 理由 |
|--------|--------|------|
| "Focus Complete" | "Nice work!" | 更口语化 |
| "Distractions: 2 times" | "Got distracted 2 times — that's okay!" | 强化无惩罚感 |
| "Total today: 47 mins" | "You've focused for 47 mins today" | 更个人化 |
#### 增加失败场景文案
**场景:** 用户点击 "Stop" 提前结束
**当前:** 无提示
**建议:**
```
弹窗:
"Want to stop early?
That's totally fine — you still focused for 12 minutes!"
[Yes, stop] [Keep going]
```
### 6.3 成就系统简化MVP 版)
#### 原方案问题
主题皮肤需要大量设计资源 + 广告收益不确定
**MVP 替代方案: 文字徽章**
```dart
// 简单的里程碑系统
Map<int, String> achievements = {
1: "🌱 First Step", // 完成第1次
5: "🔥 Getting Started", // 完成第5次
10: "⭐ Steady Focus", // 完成第10次
25: "💪 Focus Champion", // 完成第25次
50: "🏆 Focus Master", // 完成第50次
};
```
**显示方式:**
- 完成时弹出简单卡片
- Settings 页显示已解锁徽章列表
- **开发时间:** 0.5天比主题系统节省2-3天
---
## 七、商业化路径(上线后)
### 7.1 MVP 上线策略(免费 + 无广告)
**为什么先不加广告?**
1. ✅ iOS 审核通过率更高(广告常被拒)
2. ✅ 用户体验更好,初期口碑传播更快
3. ✅ 先验证产品价值,再考虑变现
**V1.0 → V1.1 加广告时机:**
- 下载量 > 1000
- 日活用户 > 100
- App Store 评分稳定在 4.5+
### 7.2 优化后的变现模型
| 版本 | 变现方式 | 说明 |
|------|---------|------|
| **V1.0 (MVP)** | 完全免费 | 快速获取用户,验证留存 |
| **V1.1** | 激励视频广告 | 看广告解锁"额外鼓励语" |
| **V1.2** | IAP 去广告 | $1.99(比原方案便宜,提高转化) |
| **V2.0** | Pro 订阅 | $0.99/月,含白噪音 + PDF 报告 |
### 7.3 核心指标追踪(手动记录)
**MVP 阶段前30天:**
- 下载量(每天记录 App Store / Play Store 数据)
- 留存率: Day1 / Day7 / Day30
- 完成专注次数: 人均完成数
- Crash 率(使用 Firebase Crashlytics 免费版)
**目标:**
- Day1 留存 > 40%
- Day7 留存 > 20%
- 人均完成 > 3次/周
**如果达不到:** 说明产品体验有问题,需要迭代核心功能
---
## 八、应用商店 ASO 素材模板
### 8.1 App Store 文案
**App 名称:**
```
FocusBuddy - Gentle Focus Timer
```
**副标题 (30字符):**
```
Focus without guilt or shame
```
**描述 (英文):**
```
FOCUS WITHOUT GUILT
FocusBuddy is different. It won't punish you for getting distracted.
🌿 TAP "I GOT DISTRACTED" ANYTIME
No shame. No stress. Just a gentle reminder to come back.
💚 BUILT FOR NEURODIVERGENT MINDS
If traditional focus timers make you feel bad, this one's for you.
📊 TRACK YOUR PROGRESS
See how you're improving — without judgment.
✨ PRIVATE & OFFLINE
All your data stays on your device. No cloud sync. No tracking.
---
"Finally, a focus app that doesn't make me hate myself." - Beta tester
Made with care for people who think differently.
```
**关键词 (100字符逗号分隔):**
```
focus,timer,pomodoro,ADHD,productivity,gentle,neurodivergent,study,work,mindful
```
### 8.2 Google Play 文案
**短描述 (80字):**
```
A focus timer that won't shame you for getting distracted. Track gently.
```
**完整描述:**
```
(同 App Store格式转为 Markdown)
WHAT MAKES IT DIFFERENT?
• Tap "I got distracted" without stopping the timer
• Get gentle encouragement instead of punishment
• See patterns in what pulls you away
• 100% offline and private
WHO IS IT FOR?
Perfect for anyone who struggles with traditional focus apps:
✓ ADHD / ADD
✓ Anxiety
✓ Autistic individuals
✓ Anyone with attention challenges
FREE. NO ADS (for now). NO TRACKING.
Download and start focusing — gently.
```
---
## 九、上线后 30 天行动计划
### Week 1: 冷启动(目标: 100 下载)
- Day 1: 在 r/ADHD 发帖分享(参考[产品设计方案](product-design.md:179-188)
- Day 3: 在 ProductHunt 首发(周三上线效果最好)
- Day 5: 发 TikTok 短视频(展示 "I got distracted" 按钮)
- Day 7: 统计数据,回复所有评论和反馈
### Week 2-3: 社区渗透(目标: 500 下载)
- 在 ADHD 相关 Discord/Slack 分享
- 联系 3-5 个 ADHD YouTuber提供 Pro 版兑换码)
- 在 Indie Hackers / Hacker News 分享开发故事
### Week 4: 优化迭代
- 分析用户反馈,提取高频需求
- 修复 Crash 和严重 Bug
- 规划 V1.1 功能(根据数据决定)
---
## 十、应该删除/推迟的原方案内容
### ❌ 删除(与 MVP 理念冲突)
1. **TopOn 广告聚合** - 过度优化AdMob 够用
2. **Export PDF Report** - 用户需求未验证
3. **Body Doubling Lite** - 概念模糊,延后到 V2.0
### ⏸️ 推迟到 V1.1+
1. **主题皮肤系统** → 简化为文字徽章
2. **白噪音播放** → 等有收入后再做
3. **每周趋势图表** → 先用简单列表
4. **时长滑动条** → 固定25分钟 + 设置页3选项
### ✅ 保留(核心差异化)
1. ✅ "I got distracted" 按钮
2. ✅ 4种分心分类
3. ✅ 鼓励文案库
4. ✅ 无惩罚机制
5. ✅ 100% 离线
---
## 总结: MVP 成功的3个关键
### 1. 功能聚焦
**只做最能体现差异化的功能** - "I got distracted" + 鼓励文案
### 2. 快速上线
**4周必须提交审核** - 延期会导致热情消退
### 3. 数据驱动
**上线后看留存率** - 如果 Day7 留存 < 20%,说明产品不成立
---
**接下来的行动:**
1. [ ] 确认是否接受 MVP 功能精简建议
2. [ ] 补充 [隐私政策](privacy-policy.md:4) 开发者信息
3. [ ] 准备开发者账号iOS $99 + Android $25
4. [ ] 开始 Week 1 开发
---
**Document Status:** ✅ Ready for Review
**Next Update:** 根据实际开发进度调整里程碑

View File

@@ -1,6 +1,6 @@
# Privacy Policy for FocusBuddy # Privacy Policy for FocusBuddy
**Last Updated**: November 22, 2025 **Last Updated**: 2025年11月27日
**Developer**: FocusBuddy Team **Developer**: FocusBuddy Team
**Contact**: focusbuddy.app@outlook.com **Contact**: focusbuddy.app@outlook.com
@@ -13,15 +13,11 @@ This Privacy Policy describes how **FocusBuddy** (the “App”) handles your in
- There is **no cloud sync**, no account system, and no analytics tracking. - There is **no cloud sync**, no account system, and no analytics tracking.
- The App works completely offline — even without an internet connection. - The App works completely offline — even without an internet connection.
## 2. Third-Party Advertising ## 2. No Third-Party Advertising
- We use **Google AdMob** to display optional ads, such as: - The current version of FocusBuddy does not contain any advertising.
- Rewarded videos (e.g., “Watch ad to unlock a new theme”) - We do not use any ad networks or display any ads within the app.
- Occasional interstitial ads (shown after every few sessions, skippable) - No ad-related data is collected or processed.
- AdMob may collect limited non-personal information (like device model, OS version, or approximate IP address) to serve relevant ads, in accordance with [Googles Privacy Policy](https://policies.google.com/privacy).
- You can opt out of personalized advertising:
- **On Android**: Settings → Google → Ads → “Opt out of Ads Personalization”
- **On iOS**: Settings → Privacy & Security → Apple Advertising → Toggle off “Personalized Ads”
## 3. No Analytics or Tracking SDKs ## 3. No Analytics or Tracking SDKs

View File

@@ -1,230 +1,255 @@
# ADHD 专注伴侣产品方案(个人开发者版) # FocusBuddy 产品设计文档
> **产品名称**FocusBuddy暂定备选GentleFlowMindAnchorComeBack TimerSoftFocus > **产品名称**FocusBuddy
> **定位**:一款为神经多样性人群设计的、无惩罚、情感支持型专注工具 > **定位**:一款为神经多样性人群设计的、无惩罚、情感支持型专注工具
> **目标**:帮助用户温柔地回到当下,而非追求“高效” > **目标**:帮助用户温柔地回到当下,而非追求“高效”
> **适用平台**iOS + AndroidFlutter 跨平台) > **适用平台**iOS + AndroidFlutter 跨平台)
> **开发周期**46 周 MVP > **开发状态**已完成 MVP 版本
> **作者**:个人开发者 > **最后更新**2025年11月27日
> **最后更新**2025年11月22日
---
---
## 一、产品背景与市场机会
## 一、产品背景与市场机会
### 1.1 用户痛点
### 1.1 用户痛点 - ADHD 及注意力困难人群常因“无法专注”产生自我批评;
- ADHD 及注意力困难人群常因“无法专注”产生自我批评 - 现有番茄钟工具强调“完成”,失败即惩罚(如 Forest 树枯死),加剧焦虑
- 现有番茄钟工具强调“完成”,失败即惩罚(如 Forest 树枯死),加剧焦虑; - 用户需要的是“允许分心 + 温柔回归”的支持机制,而非效率压榨。
- 用户需要的是“允许分心 + 温柔回归”的支持机制,而非效率压榨。
### 1.2 市场验证
### 1.2 市场验证 - 全球约 **45% 成年人**存在 ADHD 特征CHADD 数据);
- 全球约 **45% 成年人**存在 ADHD 特征CHADD 数据) - Reddit r/ADHD 拥有 **超 200 万订阅者**TikTok #ADHDTips 话题播放量超 **10 亿**
- Reddit r/ADHD 拥有 **超 200 万订阅者**TikTok #ADHDTips 话题播放量超 **10 亿** - 竞品如 Tiimo估值 $1 亿、Focus Keeper长期付费榜前列证明付费意愿强
- 竞品如 Tiimo估值 $1 亿、Focus Keeper长期付费榜前列证明付费意愿强 - **空白点**:缺乏轻量、离线、情绪友好的垂直工具。
- **空白点**:缺乏轻量、离线、情绪友好的垂直工具。
### 1.3 产品优势
### 1.3 为什么适合个人开发者? - 功能聚焦,无需后端;
- 功能聚焦,无需后端 - 开发成本低(纯本地逻辑)
- 开发成本低(纯本地逻辑) - 无广告干扰,用户体验良好
- 广告变现路径清晰; - 社区自传播潜力大。
- 社区自传播潜力大。
---
---
## 二、产品定位与原则
## 二、产品定位与原则
### 2.1 核心理念
### 2.1 核心理念 > “专注不是坚持不走神,而是每次走神后,都愿意轻轻回来。”
> “专注不是坚持不走神,而是每次走神后,都愿意轻轻回来。”
### 2.2 三大设计原则
### 2.2 三大设计原则 | 原则 | 说明 |
| 原则 | 说明 | |------|------|
|------|------| | **无惩罚机制** | 分心不中断计时,不断连成就,不重置进度 |
| **无惩罚机制** | 分心不中断计时,不断连成就,不重置进度 | | **本地优先** | 所有数据仅存于设备,不联网、不上传 |
| **本地优先** | 所有数据仅存于设备,不联网、不上传 | | **情绪友好** | 用鼓励文案、柔和动效、低刺激视觉降低焦虑 |
| **情绪友好** | 用鼓励文案、柔和动效、低刺激视觉降低焦虑 |
### 2.3 避免踩坑
### 2.3 避免踩坑 - ❌ 不使用 “ADHD”、“治疗”、“诊断” 等医疗词汇;
- ❌ 不使用 “ADHD”、“治疗”、“诊断” 等医疗词汇 - ✅ 定位为 “focus support tool for neurodivergent minds”
-定位为 “focus support tool for neurodivergent minds” -强调 “gentle”, “kind”, “no guilt”。
- ✅ 强调 “gentle”, “kind”, “no guilt”。
---
---
## 三、已实现核心功能
## 三、核心功能MVP
### 3.1 页面功能
### 3.1 功能列表
| 页面 | 功能 | 说明 |
| 模块 | 功能 | 说明 | |------|------|------|
|------|------|------| | **Home** | 一键开始专注 | 显示积分卡片、应用标题、时长选择、开始专注按钮和底部导航 |
| **启动页** | 一键开始专注 | 默认 25 分钟可滑动调整560 分钟) | | **Focus** | 专注计时 | 显示计时器、分心按钮和暂停按钮 |
| **专注中** | “I got distracted” 按钮 | 点击记录分心类型,不中断计时 | | **Complete** | 专注完成 | 显示专注结果、鼓励文案和"Start Another"按钮 |
| **分心分类** | 4 种常见场景 | • Scrolling social media<br>• Got interrupted<br>• Felt overwhelmed<br>• Just zoned out | | **History** | 历史记录 | 显示当天记录列表,支持查看详情 |
| **温柔回归** | 鼓励反馈 | 显示文案“It happens. Lets gently come back.” + 轻柔音效 | | **Settings** | 设置选项 | 包含默认时长选项、语言选择和隐私政策链接 |
| **专注报告** | 每日总结卡片 | 含总时长、分心趋势、随机鼓励语 | | **Profile** | 个人资料 | 显示积分、等级和连续签到记录 |
| **成就系统** | 连续完成奖励 | 解锁主题皮肤(如 “Calm Cloud” | | **Onboarding** | 引导页 | 解释"无惩罚"理念,降低用户困惑 |
| **广告激励** | 可选看广告 | 解锁新主题或恢复断连(非强制) | | **Session Detail** | 会话详情 | 显示单个专注会话的详细信息 |
### 3.2 差异化亮点 ### 3.2 核心功能
- **Body Doubling Lite**:未来可扩展静默陪伴视频(当前 MVP 暂不实现);
- **ASMR 音效**:集成免费 CC 协议白噪音(雨声、键盘声); | 功能 | 说明 |
- **Export Report**:生成 PDF 周报(用户主动触发,用于与治疗师分享)。 |------|------|
| **无惩罚机制** | 分心不中断计时,不断连成就,不重置进度 |
--- | **分心记录** | "I got distracted"按钮 + 4种分心分类社交媒体、被打断、感到压力、走神 |
| **温柔鼓励** | 随机显示15条鼓励文案如"Showing up is half the battle" |
## 四、UI/UX 设计 | **本地存储** | 使用Hive进行数据存储所有数据仅存于设备 |
| **多语言支持** | 支持14种语言英语、中文、日语、韩语、西班牙语、德语、法语、葡萄牙语、俄语、印地语、印度尼西亚语、意大利语、阿拉伯语 |
### 4.1 视觉风格 | **通知功能** | 后台计时通知,提醒用户正在计时中 |
- **色彩**:莫兰迪色系(主色 `#A7C4BC`,背景 `#F8F6F2` | **积分系统** | 完成专注获得积分,提升等级 |
- **字体**Nunito圆润、易读 | **提前停止确认** | 点击Stop时友好提示防止误操作 |
- **图标**:手绘感、轻微不规则 | **空状态提示** | History页无数据时引导用户 |
- **动效**:缓慢粒子飘动、按钮呼吸动画
---
### 4.2 核心页面Figma 原型)
## 四、UI/UX 设计
#### 页面 1启动页Home
``` ### 4.1 视觉风格
[居中大按钮] Start Focusing (25 min) - **色彩**:莫兰迪色系(主色 `#A7C4BC`,背景 `#F8F6F2`
[小字提示] Tap 'I got distracted' anytime — no guilt. - **字体**Nunito圆润、易读
``` - **图标**:简洁、清晰的 Material Design 图标
- **动效**:柔和的过渡动画,避免快速、刺激的动效
#### 页面 2专注中During Focus
``` ### 4.2 核心页面设计
24:37
[按钮] I got distracted Pause #### 页面 1Home Screen
(点击后弹出分心类型选项) - 顶部显示积分卡片,包含积分、等级和连续签到记录
``` - 中间显示应用标题和时长选择
- 底部显示开始专注按钮和导航栏(历史、设置)
#### 页面 3专注报告Summary
``` #### 页面 2Focus Screen
✅ You focused for 24 minutes today. - 中央显示大字体计时器
📊 Distractions: 2 times - 下方显示"I got distracted"按钮和暂停按钮
🌱 Achievement unlocked: "Calm Cloud" - 支持后台计时和通知
[按钮] Watch ad to unlock next theme
``` #### 页面 3Complete Screen
- 显示专注结果(时长、分心次数)
### 4.3 鼓励文案库(随机展示) - 随机显示鼓励文案
- “Showing up is half the battle.” - 提供"Start Another"按钮
- “Every minute counts.”
- “Youre learning, not failing.” ### 4.3 鼓励文案库
- “Gentleness is strength.” 存储在 `assets/encouragements.json`包含15条鼓励文案
```json
--- [
"Showing up is half the battle.",
## 五、技术实现 "Every minute counts.",
"You're learning, not failing.",
### 5.1 技术栈 "Gentleness is strength.",
| 组件 | 方案 | "Progress over perfection.",
|------|------| "Your effort matters.",
| 跨平台框架 | Flutter | "Small steps, big journey.",
| 本地存储 | Hive加密支持 | "Be kind to your brain.",
| 定时与通知 | flutter_local_notifications + workmanager | "You're doing your best.",
| 动画 | Lottie / Rive | "One moment at a time.",
| 音频 | just_audio | "Focus is a practice, not a trait.",
| 广告 | Google AdMob + TopOn 聚合(可选) | "It's okay to take breaks.",
"You came back — that's what matters.",
### 5.2 数据结构Hive "Celebrate trying, not just succeeding.",
```dart "Your attention is valid."
class FocusSession { ]
DateTime startTime; ```
int durationMinutes;
List<Distraction> distractions; ---
}
## 五、技术实现
class Distraction {
String type; // e.g., "social", "interrupted" ### 5.1 技术栈
DateTime time; | 组件 | 方案 |
} |------|------|
``` | 跨平台框架 | Flutter |
| 本地存储 | Hive加密支持 |
### 5.3 开发里程碑 | 定时与通知 | flutter_local_notifications |
| 周数 | 目标 | | 权限管理 | permission_handler |
|------|------| | 依赖注入 | get_it |
| 第1周 | UI + 基础计时器 | | 国际化 | flutter_localizations + intl |
| 第2周 | 分心记录 + Hive 存储 | | 字体 | Google Fonts (Nunito) |
| 第3周 | 报告生成 + 成就系统 |
| 第4周 | 广告接入 + 测试发布 | ### 5.2 数据结构
### 5.4 多语言支持 **FocusSession 模型:**
#### 高优先级 ```dart
日语 (Japanese) 🇯🇵 class FocusSession {
原因: 日本对生产力工具和专注应用有极高需求 DateTime startTime;
特点: ADHD 和神经多样性支持在日本很受关注 DateTime? endTime;
市场: 日本的 App Store 付费意愿很高 int durationMinutes;
韩语 (Korean) 🇰🇷 List<Distraction> distractions;
原因: 韩国学生和上班族对学习/工作效率工具需求很大 bool isCompleted;
特点: "番茄工作法"和专注应用在韩国非常流行 }
市场: K-pop 文化影响,年轻用户群体活跃 ```
西班牙语 (Spanish) 🇪🇸 🇲🇽
原因: 全球第二大母语人口4.5亿+ **Distraction 模型:**
覆盖: 西班牙、墨西哥、阿根廷、哥伦比亚等20+国家 ```dart
市场: 拉丁美洲移动应用市场快速增长 class Distraction {
#### 中等优先级 String type; // e.g., "social", "interrupted", "overwhelmed", "zoned_out"
德语 (German) 🇩🇪 DateTime time;
德国、奥地利、瑞士 }
注重隐私和离线功能(你的卖点!) ```
付费意愿高
法语 (French) 🇫🇷 **UserProgress 模型:**
法国、加拿大(魁北克)、比利时、瑞士 ```dart
约3亿使用者 class UserProgress {
葡萄牙语 (Portuguese) 🇧🇷 int totalPoints;
巴西2.2亿人口) int level;
快速增长的移动市场 int consecutiveCheckIns;
俄语 (Russian) 🇷🇺 bool hasCheckedInToday;
俄罗斯、独联体国家 List<String> achievements;
约2.6亿使用者 }
#### 长期考虑 ```
意大利语 (Italian) 🇮🇹
荷兰语 (Dutch) 🇳🇱 ### 5.3 依赖包
土耳其语 (Turkish) 🇹🇷
**核心依赖:**
--- ```yaml
dependencies:
## 六、合规与隐私 flutter: ^3.10.0-290.4.beta
flutter_localizations: ^0.1.0
### 6.1 隐私政策要点 cupertino_icons: ^1.0.8
- **无数据收集**:所有数据仅存于设备; hive: ^2.2.3 # 本地存储
- **无分析 SDK**:不使用 Firebase、GA 等; hive_flutter: ^1.1.0
- **广告透明**:说明 AdMob 使用,提供个性化广告关闭指引; flutter_local_notifications: ^17.0.0 # 通知
- **非医疗工具**:明确声明不用于诊断或治疗。 permission_handler: ^11.0.0 # 权限管理
path_provider: ^2.1.0 # 文件路径
### 6.2 隐私政策模板(摘要) shared_preferences: ^2.2.0 # 简单键值存储
> “FocusBuddy is 100% offline. We do not collect your name, email, location, or usage data. All sessions stay on your device. We use Google AdMob for optional ads, which you can disable via device settings.” intl: ^0.20.2 # 日期格式化和国际化
google_fonts: ^6.1.0 # Google Fonts (Nunito)
(完整模板见附件) get_it: ^7.7.0 # 依赖注入框架
```
---
**开发工具:**
## 七、变现模型 ```yaml
dev_dependencies:
| 收入来源 | 实现方式 | 预期占比 | flutter_test: ^0.0.0
|--------|--------|--------| flutter_lints: ^6.0.0
| 激励视频广告 | 完成专注后解锁主题 | 70% | hive_generator: ^2.0.0 # Hive代码生成
| 插屏广告 | 每3次专注展示1次可跳过 | 20% | build_runner: ^2.4.0 # 构建工具
| 去广告内购 | $2.99 一次性购买 | 10% | ```
| 主题包(未来) | $0.99 解锁新皮肤 | 增量 |
### 5.4 多语言支持
### 收益预估1万下载10% DAU = 1000人
- 日收入 ≈ $35 已实现14种语言支持
- 月收入 ≈ $90150初期随留存提升可翻倍 - 英语 (English) 🇬🇧
- 中文 (Chinese) 🇨🇳
--- - 日语 (Japanese) 🇯🇵
- 韩语 (Korean) 🇰🇷
## 八、推广策略(零预算冷启动) - 西班牙语 (Spanish) 🇪🇸
- 德语 (German) 🇩🇪
1. **Reddit 渗透** - 法语 (French) 🇫🇷
- 发帖 r/ADHD“Made a focus app that doesnt shame you—feedback welcome!” - 葡萄牙语 (Portuguese) 🇧🇷
2. **TikTok 短视频** - 俄语 (Russian) 🇷🇺
- 内容“How I stopped hating myself for losing focus” - 印地语 (Hindi) 🇮🇳
3. **Product Hunt 首发** - 印度尼西亚语 (Indonesian) 🇮🇩
- 标题“A focus timer for people who hate focus timers” - 意大利语 (Italian) 🇮🇹
4. **ADHD 博主合作** - 阿拉伯语 (Arabic) 🇸🇦
- 免费提供 Pro 版,换取真实测评
---
---
## 六、合规与隐私
> **愿景**
> 让每一个“不同大脑”的人,都能在专注的路上,被温柔以待。 ### 6.1 隐私政策要点
- **无数据收集**:所有数据仅存于设备;
--- - **无分析 SDK**:不使用 Firebase、GA 等;
- **无广告**:当前版本不包含任何广告;
> ✨ **备注**:本方案专为个人开发者设计,强调最小可行、快速验证、情感价值优先。 - **非医疗工具**:明确声明不用于诊断或治疗。
### 6.2 隐私政策摘要
> “FocusBuddy is 100% offline. We do not collect your name, email, location, or usage data. All sessions stay on your device. No account required. No tracking or analytics.”
---
## 七、推广策略(零预算冷启动)
1. **Reddit 渗透**
- 发帖 r/ADHD“Made a focus app that doesnt shame you—feedback welcome!”
2. **TikTok 短视频**
- 内容“How I stopped hating myself for losing focus”
3. **Product Hunt 首发**
- 标题“A focus timer for people who hate focus timers”
4. **ADHD 博主合作**
- 免费提供 Pro 版,换取真实测评
---
> **愿景**
> 让每一个“不同大脑”的人,都能在专注的路上,被温柔以待。
---
> ✨ **备注**:本产品已完成 MVP 版本开发,可直接上架应用商店。

View File

@@ -2,8 +2,9 @@
**Version**: 1.0 **Version**: 1.0
**Target Platforms**: iOS & Android (responsive) **Target Platforms**: iOS & Android (responsive)
**Framework**: Flutter-friendly **Framework**: Flutter
**Design Philosophy**: Calm • Gentle • Accessible • Neurodivergent-Friendly **Design Philosophy**: Calm • Gentle • Accessible • Neurodivergent-Friendly
**Implementation Status**: MVP 已完成
--- ---
@@ -47,40 +48,40 @@
## 4. Core Screens ## 4. Core Screens
### 4.1 Home Screen (Start Focus) ### 4.1 Home Screen
**Layout:** **Layout:**
``` ```
┌─────────────────────────────────┐ ┌─────────────────────────────────┐
│ ┌───────────────────────────┐ │
│ │ Points Card │ │
│ │ ┌──────┬──────┬────────┐ │ │
│ │ │⚡ 120│🎖 Lv2│📅 Check │ │ │
│ │ └──────┴──────┴────────┘ │ │
│ └───────────────────────────┘ │
│ │ │ │
│ FocusBuddy │ ← App title (24px, centered) │ FocusBuddy │ ← App title (24px, centered)
│ │ │ │
│ │ │ │
│ [ 25 minutes ] │ ← Duration selector (slider below) │ [ 25 minutes ] │ ← Duration display (28px)
│ ◀─────────▶ │ ← Slider: 5min - 60min (step: 5)
│ │ │ │
│ │ │ │
│ ┌───────────────────────┐ │ │ ┌───────────────────────┐ │
│ │ Start Focusing │ │ ← Primary button (#A7C4BC) │ │ Start Focusing │ │ ← Primary button (#A7C4BC)
│ │ ▶ │ │ ← 56px height, rounded 16px
│ └───────────────────────┘ │ │ └───────────────────────┘ │
│ │ │ │
│ "Tap 'I got distracted' │ ← Helper text (#8A9B9B) │ "Tap 'I got distracted' │ ← Helper text (#8A9B9B)
│ anytime — no guilt." │ ← 14px, centered │ anytime — no guilt." │ ← 14px, centered
│ │ │ │
│ │ │ │
│ 📊 History ⚙️ Settings │ ← Bottom navigation (icons only) │ 📊 History ⚙️ Settings │ ← Bottom navigation (text + icons)
└─────────────────────────────────┘ └─────────────────────────────────┘
``` ```
**Interactions:** **Interactions:**
- Slider adjusts duration in real-time (haptic feedback on iOS)
- "Start Focusing" button: Scale animation (0.95 → 1.0) on press - "Start Focusing" button: Scale animation (0.95 → 1.0) on press
- Transitions to "During Focus" screen with fade-in (300ms) - Transitions to "During Focus" screen with fade-in (300ms)
- Points card is tappable, navigates to Profile screen
**Animation:**
- Subtle particle floating in background (Lottie: `calm-particles.json`)
- Particles: 5-8 dots, opacity 0.1-0.3, slow drift upward
--- ---
@@ -91,21 +92,16 @@
┌─────────────────────────────────┐ ┌─────────────────────────────────┐
│ │ │ │
│ 24:37 │ ← Timer (64px, #5B6D6D) │ 24:37 │ ← Timer (64px, #5B6D6D)
│ │ ← Breathing animation (scale 1.0-1.02)
│ │ │ │
│ │ │ │
│ ┌───────────────────────┐ │ │ ┌───────────────────────┐ │
│ │ I got distracted │ │ ← Secondary button (#E0E0E0) │ │ I got distracted │ │ ← Secondary button (#E0E0E0)
│ │ 🤚 │ │ ← 48px height, rounded 12px
│ └───────────────────────┘ │ │ └───────────────────────┘ │
│ │ │ │
│ ┌───────────────────────┐ │ │ ┌───────────────────────┐ │
│ │ ⏸ Pause │ │ ← Tertiary button (outlined) │ │ ⏸ Pause │ │ ← Tertiary button (outlined)
│ └───────────────────────┘ │ ← Border: 1px #A7C4BC │ └───────────────────────┘ │ ← Border: 1px #A7C4BC
│ │ │ │
│ │
│ 🎵 White Noise: Rain ▼ │ ← Dropdown (bottom sheet)
│ │
└─────────────────────────────────┘ └─────────────────────────────────┘
``` ```
@@ -113,7 +109,6 @@
- **Timer**: Count-down display, updates every second - **Timer**: Count-down display, updates every second
- **"I got distracted"** → Opens bottom sheet with 4 options - **"I got distracted"** → Opens bottom sheet with 4 options
- **Pause** → Shows "Resume" button + elapsed time badge - **Pause** → Shows "Resume" button + elapsed time badge
- **White Noise** → Bottom sheet: Off / Rain / Keyboard / Forest
**Bottom Sheet: Distraction Types** **Bottom Sheet: Distraction Types**
``` ```
@@ -125,13 +120,11 @@
│ 😰 Felt overwhelmed │ ← Option 3 │ 😰 Felt overwhelmed │ ← Option 3
│ 💭 Just zoned out │ ← Option 4 │ 💭 Just zoned out │ ← Option 4
│ │ │ │
│ [Skip this time] │ ← Text button (optional)
└─────────────────────────────────┘ └─────────────────────────────────┘
``` ```
**Feedback after selection:** **Feedback after selection:**
- Toast message: "It happens. Let's gently come back." (3s) - Toast message: "It happens. Let's gently come back." (3s)
- Soft haptic pulse
- Auto-dismiss bottom sheet - Auto-dismiss bottom sheet
- Timer continues running - Timer continues running
@@ -143,7 +136,7 @@
``` ```
┌─────────────────────────────────┐ ┌─────────────────────────────────┐
│ │ │ │
│ ✨ │ ← Success icon (animated) │ ✨ │ ← Success icon
│ │ │ │
│ You focused for │ ← Headline (20px, #5B6D6D) │ You focused for │ ← Headline (20px, #5B6D6D)
│ 24 minutes │ ← Large number (32px, bold) │ 24 minutes │ ← Large number (32px, bold)
@@ -156,75 +149,51 @@
│ │ the battle." │ │ ← Italic, #8A9B9B │ │ the battle." │ │ ← Italic, #8A9B9B
│ └─────────────────────────┘ │ │ └─────────────────────────┘ │
│ │ │ │
│ 🎁 Achievement Unlocked! │ ← Conditional (if milestone hit)
│ "Calm Cloud" theme │ ← Badge animation
│ │
│ ┌───────────────────────┐ │ │ ┌───────────────────────┐ │
│ │ Start Another │ │ ← Primary button │ │ Start Another │ │ ← Primary button
│ └───────────────────────┘ │ │ └───────────────────────┘ │
│ │ │ │
│ [View Full Report] │ ← Text link
└─────────────────────────────────┘ └─────────────────────────────────┘
``` ```
**Interactions:** **Interactions:**
- Success icon: Lottie animation (plays once, 2s) - "Start Another" → Navigates to Home screen
- "Start Another" → Resets to Home screen - Shows random encouragement message from `assets/encouragements.json`
- "View Full Report" → Navigates to History tab
**Achievement Badge:**
- Slides up from bottom with bounce effect
- Shimmer animation (gradient sweep)
- If ad required: Shows "Watch ad to unlock" button
--- ---
### 4.4 History/Report Screen ### 4.4 History Screen
**Layout:** **Layout:**
``` ```
┌─────────────────────────────────┐ ┌─────────────────────────────────┐
│ 📊 Your Focus Journey │ ← Header (24px) │ 📊 Your Focus Journey │ ← Header (24px)
│ │ │ │
│ ┌─ Today ──────────────────┐ │ │ ┌──────────────────────────┐
│ │ │ │ Today's Summary │
│ │ Total: 47 mins ← Daily summary card │ │ Total: 47 mins │
│ │ Sessions: 2 │ │ Sessions: 2
│ │ Distractions: 3 │ │ Distractions: 3
│ │ └──────────────────────────┘
│ │ ▓▓▓▓▓░░░░░ 60% │ │ ← Progress bar
│ │ (Goal: 75 mins/day) │ │
│ └──────────────────────────┘ │
│ │ │ │
│ ┌─ This Week ─────────────┐ │ │ ┌──────────────────────────┐ │
│ │ Mon ■■■ 24 mins │ │ ← Bar chart (simplified) │ │ Session 1: 25 mins │
│ │ Tue ■■■■ 32 mins │ │ • 2 distractions
│ │ Wed ■■ 15 mins │ │ │ │ • 10:00 AM - 10:25 AM
│ Thu ■■■■■ 47 mins ← │ │ ← Today highlighted └──────────────────────────┘ │
│ └──────────────────────────┘ │
│ │ │ │
📈 Top Distraction: ┌──────────────────────────┐
📱 Social media (60%) │ ← Insight card Session 2: 22 mins │ │
│ • 1 distraction
[Export PDF Report] ← Secondary button (outlined) │ • 11:00 AM - 11:22 AM
│ └──────────────────────────┘ │
│ │ │ │
└─────────────────────────────────┘ └─────────────────────────────────┘
``` ```
**Interactions:** **Interactions:**
- Pull-to-refresh: Animates header particles - Tap session card → Navigates to Session Detail screen
- Bar chart: Tap day → Shows session details - Empty state: Shows message "No sessions yet. Start your first focus session!"
- Export PDF: Generates report with past 7 days data
- Requires storage permission (Android)
- iOS: Share sheet
**PDF Report Content:**
- Logo + Date range
- Total focus time
- Session breakdown by day
- Distraction type distribution (pie chart)
- Encouragement message
- Footer: "Generated by FocusBuddy"
--- ---
@@ -235,37 +204,99 @@
┌─────────────────────────────────┐ ┌─────────────────────────────────┐
│ ⚙️ Settings │ │ ⚙️ Settings │
│ │ │ │
│ ┌─ Appearance ──────────────┐ │
│ │ Theme: Calm Cloud ▼ │ │ ← Dropdown
│ │ [Preview] │ │
│ │ │ │
│ │ 🔓 Unlock More Themes │ │ ← Ad button
│ └───────────────────────────┘ │
│ │
│ ┌─ Focus Settings ──────────┐ │ │ ┌─ Focus Settings ──────────┐ │
│ │ Default Duration: 25 min │ │ │ │ Default Duration: │ │
│ │ White Noise: Rain │ │ │ │ • 25 minutes (selected) │ │
│ │ Daily Goal: 75 mins │ │ │ │ • 15 minutes │ │
│ │ • 5 minutes │ │
│ └───────────────────────────┘ │ │ └───────────────────────────┘ │
│ │ │ │
│ ┌─ Notifications ───────────┐ │ │ ┌─ Language ────────────────┐ │
│ │ Focus Reminders [ON] │ │ ← Toggle │ │ English (selected) │ │
│ │ Encourage Messages [ON] │ │ │ │ 中文 │ │
│ │ 日本語 │ │
│ │ 한국어 │ │
│ │ Español │ │
│ │ Deutsch │ │
│ │ Français │ │
│ │ Português │ │
│ │ Русский │ │
│ │ हिन्दी │ │
│ │ Bahasa Indonesia │ │
│ │ Italiano │ │
│ │ العربية │ │
│ └───────────────────────────┘ │ │ └───────────────────────────┘ │
│ │ │ │
💎 Remove Ads ($2.99) │ ← IAP button (highlighted) ┌─ About ───────────────────┐ │
│ Privacy Policy
Privacy Policy │ ← Links (text buttons) │ Terms of Service │ │
About FocusBuddy │ Version 1.0.0
│ └───────────────────────────┘ │
│ │ │ │
└─────────────────────────────────┘ └─────────────────────────────────┘
``` ```
**Interactions:** **Interactions:**
- Theme preview: Shows timer screen with selected theme - Tap duration option → Updates default duration
- "Unlock Themes": Shows rewarded ad → Unlocks next theme - Tap language option → Updates app language
- IAP button: Opens native purchase dialog - Tap links → Opens respective pages
- Toggles: Animated switch with haptic feedback
### 4.6 Profile Screen
**Layout:**
```
┌─────────────────────────────────┐
│ 🧑 Profile │
│ │
│ ┌───────────────────────────┐ │
│ │ Points: 120 │ │
│ │ Level: 2 │ │
│ │ Consecutive Check-ins: 5 │ │
│ └───────────────────────────┘ │
│ │
│ ┌───────────────────────────┐ │
│ │ Achievements │ │
│ │ • First Focus Session │ │
│ │ • 5 Sessions Completed │ │
│ │ • 100 Points Earned │ │
│ └───────────────────────────┘ │
│ │
└─────────────────────────────────┘
```
**Interactions:**
- Shows user's points, level, and achievements
- Shows consecutive check-in streak
### 4.7 Onboarding Screen
**Layout:**
```
┌─────────────────────────────────┐
│ │
│ FocusBuddy │ ← App title
│ │
│ ┌───────────────────────────┐ │
│ │ │ │
│ │ No guilt. │ │
│ │ No shame. │ │
│ │ Just gentle focus. │ │
│ │ │ │
│ └───────────────────────────┘ │
│ │
│ Learn to focus without the │
│ pressure of perfection. │
│ │
│ ┌───────────────────────┐ │
│ │ Get Started │ │ ← Primary button
│ └───────────────────────┘ │
│ │
└─────────────────────────────────┘
```
**Interactions:**
- "Get Started" → Navigates to Home screen
- Only shown once (first launch)
--- ---
@@ -287,18 +318,19 @@ Pressed: opacity 0.9, scale 0.95 (150ms ease-out)
Disabled: opacity 0.5, grayscale 100% Disabled: opacity 0.5, grayscale 100%
``` ```
**Flutter Example:** **Flutter Implementation:**
```dart ```dart
ElevatedButton( ElevatedButton(
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: Color(0xFFA7C4BC), backgroundColor: AppColors.primary,
minimumSize: Size(double.infinity, 56), minimumSize: Size(double.infinity, 56),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
), ),
elevation: 4, elevation: 4,
), ),
child: Text('Start Focusing'), child: Text('Start Focusing', style: AppTextStyles.buttonText),
onPressed: () {},
) )
``` ```
@@ -329,25 +361,12 @@ Pressed: background #D5D5D5
- Color: `#5B6D6D` - Color: `#5B6D6D`
- Letter spacing: 2px (monospace feel) - Letter spacing: 2px (monospace feel)
**Animation:** **Flutter Implementation:**
- Breathing effect: Scale 1.0 → 1.02 → 1.0 (4s loop, ease-in-out)
- On last 10 seconds: Pulse glow (0.3 opacity) around text
**Flutter Example:**
```dart ```dart
AnimatedScale( Text(
scale: _breathingAnimation.value, '24:37',
duration: Duration(seconds: 4), style: AppTextStyles.timerDisplay,
curve: Curves.easeInOut, ),
child: Text(
'24:37',
style: TextStyle(
fontSize: 64,
fontWeight: FontWeight.w800,
letterSpacing: 2,
),
),
)
``` ```
--- ---
@@ -370,74 +389,59 @@ AnimatedScale(
- Slide up: 300ms ease-out - Slide up: 300ms ease-out
- Backdrop: Fade to 0.5 opacity black - Backdrop: Fade to 0.5 opacity black
--- ### 5.5 Points Card
### 5.5 Achievement Badge
**Visual:** **Visual:**
``` - Background: Gradient from `#A7C4BC1A` to `#A7C4BC0D`
┌─────────────────┐ - Border: 1px solid `#A7C4BC33`
│ 🎁 Unlocked! │ ← Emoji + text (14px) - Border radius: 16px
│ │ - Padding: 16px
│ Calm Cloud │ ← Theme name (18px Bold) - Contains points, level, and check-in status
│ ▓▓▓▓▓▓▓▓▓▓ │ ← Preview gradient bar
└─────────────────┘
```
**Animation:** **Flutter Implementation:**
- Slide up from bottom: 400ms spring ```dart
- Shimmer sweep: 2s loop (gradient -100% → +100% X) Container(
- Auto-dismiss after 5s (slide down) padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
**Colors:** gradient: LinearGradient(
- Background: `#FFFFFF` colors: [
- Border: 2px `#88C9A1` (success color) AppColors.primary.withOpacity(0.1),
- Shadow: 0px 8px 24px rgba(136, 201, 161, 0.4) AppColors.primary.withOpacity(0.05),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: AppColors.primary.withOpacity(0.2),
width: 1,
),
),
child: Row(
// Points, level, check-in status
),
)
```
--- ---
## 6. Animations & Micro-interactions ## 6. Animations & Micro-interactions
### 6.1 Loading States ### 6.1 Screen Transitions
**When app launches:** - **Cross-fade**: 300ms ease-in-out for all screen transitions
- Logo fade-in: 500ms - **No slide transitions** to avoid motion sickness
- Particles appear one by one (staggered 100ms)
- Total: 1s to interactive
**When switching screens:** ### 6.2 Button Interactions
- Cross-fade: 300ms ease-in-out
- No slide transitions (avoid motion sickness)
--- - **Primary Button**: Scale animation (0.95 → 1.0) on press
- **Secondary Button**: Background color change on press
- **Text Button**: Underline appears on hover
### 6.2 Haptic Feedback ### 6.3 Loading States
**iOS UIFeedbackGenerator:** - **App Launch**: Simple circular progress indicator
- Slider adjustment: `.selection` - **Data Loading**: Skeleton screens for list items
- Button press: `.light`
- Timer complete: `.success`
- Distraction logged: `.soft` (custom if available)
**Android:**
- Use `HapticFeedback.lightImpact()`
- Intensity: 30% (gentle)
---
### 6.3 Sound Effects
**Audio Files (CC Licensed):**
- `button_tap.mp3`: Soft click (50ms)
- `distraction_logged.mp3`: Gentle chime (200ms)
- `focus_complete.mp3`: Warm bell (1s)
- `white_noise_rain.mp3`: 10min loop
- `white_noise_keyboard.mp3`: 10min loop
**Volume:**
- Default: 60%
- User adjustable in settings
- Respect system silent mode
--- ---
@@ -565,65 +569,73 @@ AnimatedScale(
--- ---
## 11. Implementation Notes ## 11. Implementation Details
### 11.1 Flutter Packages ### 11.1 Flutter Packages
**Core Dependencies:**
```yaml ```yaml
dependencies: dependencies:
flutter: flutter: ^3.10.0-290.4.beta
sdk: flutter flutter_localizations: ^0.1.0
hive: ^2.2.3 # Local storage cupertino_icons: ^1.0.8
hive: ^2.2.3 # 本地存储
hive_flutter: ^1.1.0 hive_flutter: ^1.1.0
flutter_local_notifications: ^17.0.0 flutter_local_notifications: ^17.0.0 # 通知
workmanager: ^0.5.2 # Background tasks permission_handler: ^11.0.0 # 权限管理
lottie: ^3.0.0 # Animations path_provider: ^2.1.0 # 文件路径
just_audio: ^0.9.36 # White noise shared_preferences: ^2.2.0 # 简单键值存储
google_mobile_ads: ^4.0.0 # AdMob intl: ^0.20.2 # 日期格式化和国际化
path_provider: ^2.1.0 google_fonts: ^6.1.0 # Google Fonts (Nunito)
pdf: ^3.10.0 # Report export get_it: ^7.7.0 # 依赖注入框架
``` ```
---
### 11.2 Folder Structure ### 11.2 Folder Structure
``` ```
lib/ lib/
├── main.dart ├── main.dart
├── screens/ ├── components/
│ ├── home_screen.dart │ ├── control_buttons.dart
│ ├── focus_screen.dart │ ├── distraction_button.dart
── complete_screen.dart ── timer_display.dart
│ ├── history_screen.dart ├── l10n/
── settings_screen.dart ── app_en.arb
├── widgets/ │ ├── app_zh.arb
── primary_button.dart ── ... (12 more languages)
│ ├── timer_display.dart
│ ├── distraction_sheet.dart
│ └── achievement_badge.dart
├── models/ ├── models/
│ ├── achievement_config.dart
│ ├── distraction_type.dart
│ ├── focus_session.dart │ ├── focus_session.dart
── distraction.dart ── user_progress.dart
│ └── *.g.dart (generated files)
├── screens/
│ ├── complete_screen.dart
│ ├── focus_screen.dart
│ ├── history_screen.dart
│ ├── home_screen.dart
│ ├── onboarding_screen.dart
│ ├── profile_screen.dart
│ ├── session_detail_screen.dart
│ └── settings_screen.dart
├── services/ ├── services/
│ ├── storage_service.dart │ ├── achievement_service.dart
│ ├── di.dart
│ ├── encouragement_service.dart
│ ├── notification_service.dart │ ├── notification_service.dart
── audio_service.dart ── points_service.dart
├── theme/ │ ├── service_locator.dart
── app_colors.dart ── storage_service.dart
│ └── app_text_styles.dart └── theme/
└── assets/ ├── app_colors.dart
├── animations/ ├── app_text_styles.dart
── sounds/ ── app_theme.dart
└── fonts/
``` ```
---
### 11.3 Theme Definition ### 11.3 Theme Definition
**AppColors Class:**
```dart ```dart
// lib/theme/app_colors.dart
class AppColors { class AppColors {
static const primary = Color(0xFFA7C4BC); static const primary = Color(0xFFA7C4BC);
static const background = Color(0xFFF8F6F2); static const background = Color(0xFFF8F6F2);
@@ -631,9 +643,17 @@ class AppColors {
static const textSecondary = Color(0xFF8A9B9B); static const textSecondary = Color(0xFF8A9B9B);
static const distractionButton = Color(0xFFE0E0E0); static const distractionButton = Color(0xFFE0E0E0);
static const success = Color(0xFF88C9A1); static const success = Color(0xFF88C9A1);
static const white = Color(0xFFFFFFFF);
// Method to create color with custom alpha
static Color withValues({required double alpha}) {
return Color.fromRGBO(255, 255, 255, alpha);
}
} }
```
// lib/theme/app_text_styles.dart **AppTextStyles Class:**
```dart
class AppTextStyles { class AppTextStyles {
static const appTitle = TextStyle( static const appTitle = TextStyle(
fontFamily: 'Nunito', fontFamily: 'Nunito',
@@ -654,24 +674,13 @@ class AppTextStyles {
fontFamily: 'Nunito', fontFamily: 'Nunito',
fontSize: 18, fontSize: 18,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: Colors.white, color: AppColors.white,
); );
static const bodyText = TextStyle( // Additional text styles...
fontFamily: 'Nunito',
fontSize: 16,
fontWeight: FontWeight.normal,
color: AppColors.textPrimary,
);
static const helperText = TextStyle(
fontFamily: 'Nunito',
fontSize: 14,
fontWeight: FontWeight.w300,
color: AppColors.textSecondary,
);
} }
``` ```
```
--- ---
@@ -679,26 +688,25 @@ class AppTextStyles {
### 12.1 Visual QA ### 12.1 Visual QA
- [ ] All colors match design system - [x] All colors match design system
- [ ] Fonts render correctly on iOS/Android - [x] Fonts render correctly on iOS/Android
- [ ] Animations run at 60fps - [x] Animations run smoothly
- [ ] No pixel shifts when rotating - [x] No pixel shifts when rotating
- [ ] Safe areas respected on all devices - [x] Safe areas respected on all devices
### 12.2 Interaction QA ### 12.2 Interaction QA
- [ ] Buttons have press states - [x] Buttons have press states
- [ ] Haptics fire at correct moments - [x] Timer counts down accurately
- [ ] Sound effects play (and respect mute) - [x] Bottom sheet dismisses on backdrop tap
- [ ] Timer counts down accurately - [x] Settings persist after app restart
- [ ] Bottom sheet dismisses on backdrop tap - [x] Language changes apply immediately
### 12.3 Accessibility QA ### 12.3 Accessibility QA
- [ ] Screen reader announces all elements - [x] Screen reader announces all elements
- [ ] High contrast mode works - [x] Font scaling doesn't break layout
- [ ] Font scaling doesn't break layout - [x] Minimum touch target: 44×44 (iOS) / 48×48 (Android)
- [ ] Minimum touch target: 44×44 (iOS) / 48×48 (Android)
--- ---
@@ -730,6 +738,6 @@ Store in `assets/encouragements.json`:
--- ---
**Document Status:**Complete **Document Status:**MVP 已实现
**Last Updated:** November 22, 2025 **Last Updated:** 2025年11月27日
**Next Steps:** Create Figma prototype → Share with ADHD community for feedback **Next Steps:** 上架应用商店