Compare commits

...

8 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
ytc1012
0195cdf54b 优化 2025-11-26 16:32:47 +08:00
ytc1012
96658339e1 优化 2025-11-26 16:32:18 +08:00
ytc1012
f8c4a18920 通知栏图标优化 2025-11-25 11:15:45 +08:00
65 changed files with 10207 additions and 2113 deletions

View File

@@ -43,7 +43,26 @@
"Bash(flutter clean:*)", "Bash(flutter clean:*)",
"Bash(start ms-settings:developers)", "Bash(start ms-settings:developers)",
"Bash(gradlew.bat --stop:*)", "Bash(gradlew.bat --stop:*)",
"Bash(call gradlew.bat:*)" "Bash(call gradlew.bat:*)",
"Bash(where:*)",
"Bash(gradlew.bat:*)",
"Bash(if [ -d \"android/app/build/outputs\" ])",
"Bash(then find android/app/build/outputs -type f ( -name \"*.aab\" -o -name \"*.apk\" ))",
"Bash(else echo \"outputs 目录不存在,可能还未构建过\")",
"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

@@ -0,0 +1,208 @@
# FocusBuddy 代码优化计划
## 一、架构优化
### 1. 统一服务层初始化模式
- **问题**:当前服务层使用了不同的初始化模式(静态方法、单例、普通实例),导致代码不一致
- **优化方案**
- 将所有服务统一为单例模式或依赖注入模式
- 建立服务容器,统一管理服务实例
- 代码示例:
```dart
// 统一服务初始化
class ServiceLocator {
static final ServiceLocator _instance = ServiceLocator._internal();
factory ServiceLocator() => _instance;
ServiceLocator._internal();
late StorageService storageService;
late NotificationService notificationService;
late EncouragementService encouragementService;
Future<void> initialize() async {
storageService = StorageService();
await storageService.init();
notificationService = NotificationService();
await notificationService.initialize();
await notificationService.requestPermissions();
encouragementService = EncouragementService();
await encouragementService.loadMessages();
}
}
```
### 2. 引入依赖注入
- **问题**:当前服务依赖关系不清晰,难以进行单元测试
- **优化方案**
- 引入依赖注入框架(如 get_it 或 provider
- 使服务之间的依赖关系显式化
- 提高代码的可测试性和可维护性
## 二、性能优化
### 1. 优化存储查询性能
- **问题**`StorageService.getTodaySessions()` 每次调用都会遍历所有会话,对于大量数据可能影响性能
- **优化方案**
- 添加缓存机制,缓存当天的会话数据
- 实现会话数据的索引,提高查询效率
- 代码示例:
```dart
List<FocusSession>? _todaySessionsCache;
DateTime? _cacheDate;
List<FocusSession> getTodaySessions() {
final now = DateTime.now();
final today = DateTime(now.year, now.month, now.day);
// Check if cache is valid
if (_todaySessionsCache != null && _cacheDate == today) {
return _todaySessionsCache!;
}
// Query and cache results
final sessions = _sessionsBox.values.where((session) {
final sessionDate = DateTime(
session.startTime.year,
session.startTime.month,
session.startTime.day,
);
return sessionDate == today;
}).toList();
_todaySessionsCache = sessions;
_cacheDate = today;
return sessions;
}
```
### 2. 优化通知服务性能
- **问题**:通知服务在初始化时进行了不必要的平台检查
- **优化方案**
- 延迟初始化通知服务,仅在需要时初始化
- 优化权限检查逻辑,避免重复请求权限
## 三、代码质量优化
### 1. 完善文档注释
- **问题**:部分方法缺少文档注释,降低了代码的可维护性
- **优化方案**
- 为所有公共方法添加详细的文档注释
- 说明方法的用途、参数和返回值
- 示例:
```dart
/// Save a focus session to local storage
///
/// [session] - The focus session to save
/// Returns a Future that completes when the session is saved
Future<void> saveFocusSession(FocusSession session) async {
await _sessionsBox.add(session);
_invalidateCache(); // Invalidate cache when data changes
}
```
### 2. 增强错误处理
- **问题**:部分错误处理逻辑不够完善,可能导致应用崩溃
- **优化方案**
- 为所有异步操作添加 try-catch 块
- 实现统一的错误处理机制
- 添加适当的日志记录
### 3. 提高代码模块化
- **问题**`focus_screen.dart` 代码较长480行可读性和可维护性较差
- **优化方案**
- 将长页面拆分为多个小组件
- 例如TimerDisplay、DistractionButton、ControlButtons 等
- 提高代码的复用性和可维护性
## 四、功能优化
### 1. 完善通知权限管理
- **问题**:当前通知权限请求逻辑不够完善
- **优化方案**
- 添加权限状态监听
- 当权限被拒绝时,引导用户手动开启权限
- 实现更细粒度的权限控制
### 2. 增强鼓励语服务
- **问题**:当前鼓励语服务功能比较简单
- **优化方案**
- 添加基于用户行为的个性化鼓励语
- 支持不同场景的鼓励语(开始、中途、完成)
- 允许用户添加自定义鼓励语
### 3. 优化多语言支持
- **问题**:部分硬编码字符串没有国际化
- **优化方案**
- 统一使用国际化资源
- 为所有用户可见的文本添加翻译
- 实现语言切换的即时生效
## 五、测试友好性优化
### 1. 提高代码可测试性
- **问题**:当前代码结构不利于单元测试
- **优化方案**
- 提取纯函数,便于单独测试
- 实现接口抽象,便于 mock 测试
- 添加测试覆盖率报告
### 2. 添加单元测试
- **问题**:当前项目缺少单元测试
- **优化方案**
- 为核心服务添加单元测试
- 为数据模型添加测试
- 为工具函数添加测试
## 六、UI/UX 优化
### 1. 优化页面布局
- **问题**:当前页面布局比较简单,缺乏层次感
- **优化方案**
- 添加更多的动画效果
- 优化颜色搭配和字体大小
- 实现响应式设计,适配不同屏幕尺寸
### 2. 增强用户反馈
- **问题**:当前用户反馈机制不够完善
- **优化方案**
- 添加更多的状态反馈(加载中、成功、失败)
- 优化按钮点击效果
- 添加声音和振动反馈(可选)
## 七、实施步骤
### 第一阶段基础优化1-2天
1. 统一服务层初始化模式
2. 完善文档注释
3. 增强错误处理
4. 优化存储查询性能
### 第二阶段架构优化2-3天
1. 引入依赖注入
2. 提高代码模块化
3. 优化通知服务
4. 增强鼓励语服务
### 第三阶段功能优化2-3天
1. 完善通知权限管理
2. 优化多语言支持
3. 增强用户反馈
4. 优化页面布局
### 第四阶段测试优化1-2天
1. 提高代码可测试性
2. 添加单元测试
3. 运行测试并修复问题
## 八、预期收益
1. **提高代码质量**:统一的架构、完善的文档、增强的错误处理
2. **提高性能**:优化的存储查询、更高效的服务初始化
3. **提高可维护性**:模块化的代码、清晰的依赖关系
4. **提高可测试性**:依赖注入、单元测试
5. **增强功能**:更完善的通知服务、个性化的鼓励语
6. **优化用户体验**更好的UI设计、增强的用户反馈
通过以上优化FocusBuddy 项目将变得更加健壮、高效、可维护,为后续的功能扩展和版本迭代打下坚实的基础。

284
CLAUDE.md Normal file
View File

@@ -0,0 +1,284 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
FocusBuddy is a Flutter-based focus timer app designed for neurodivergent minds, implementing a "no-punishment" philosophy. The core concept is that distractions during focus sessions are tracked but don't interrupt the timer, providing a gentler approach to productivity tracking.
## Commands
### Development Setup
```bash
# Check Flutter version and environment
flutter doctor
# Get dependencies
flutter pub get
# Generate localization files (required after editing .arb files)
flutter gen-l10n
# Generate Hive adapters (required after modifying model classes)
flutter pub run build_runner build
# Clean build artifacts
flutter clean
```
### Running the App
```bash
# List available devices/emulators
flutter devices
# Run on default device
flutter run
# Run on specific device
flutter run -d <device_id>
# Run on Chrome (web)
flutter run -d chrome
# Run with hot reload (default in debug mode)
flutter run --hot
```
### Building
```bash
# Build APK for Android
flutter build apk
# Build App Bundle for Google Play
flutter build appbundle
# Build for iOS (requires macOS)
flutter build ios
# Build for Windows
flutter build windows
```
### Testing & Analysis
```bash
# Run static analysis
flutter analyze
# Run tests
flutter test
# Check for outdated packages
flutter pub outdated
```
## Architecture
### Core Philosophy
The app follows a **no-punishment approach** to focus tracking:
- Timer continues running even when user reports a distraction
- Distractions are tracked via "I got distracted" button
- Encouragement messages replace negative feedback
- All distraction counts are for awareness, not judgment
### Key Technical Decisions
**State Management**: Simple `StatefulWidget` approach (no external state management)
- Focus session state managed locally in `FocusScreen`
- Settings and preferences stored in `shared_preferences`
- Historical data stored in Hive local database
**Data Persistence**:
- **Hive** for focus session history (typeId 0 for `FocusSession`)
- **SharedPreferences** for app settings (onboarding status, default duration, locale)
- Sessions stored with full distraction tracking but continue regardless of interruptions
**Notifications**:
- Background notifications when app is inactive during focus session
- Uses `flutter_local_notifications` with `WidgetsBindingObserver` lifecycle tracking
- Notification updates every 30 seconds when app is backgrounded
**Internationalization (i18n)**:
- Uses Flutter's official `intl` package with ARB files
- 13 supported languages (en, zh, ja, ko, es, de, fr, pt, ru, hi, id, it, ar)
- Locale can be changed at runtime without app restart via `MyApp.updateLocale()`
- Localization files generated in `lib/l10n/` via `flutter gen-l10n`
### Project Structure
```
lib/
├── main.dart # Entry point, app initialization
├── models/ # Data models
│ ├── focus_session.dart # Hive model for session data
│ ├── focus_session.g.dart # Generated Hive adapter
│ └── distraction_type.dart # Enum for distraction categories
├── screens/ # UI screens
│ ├── onboarding_screen.dart # First-time user guide
│ ├── home_screen.dart # Main screen with "Start Focus" button
│ ├── focus_screen.dart # Active timer + distraction button
│ ├── complete_screen.dart # Post-session summary
│ ├── history_screen.dart # Past sessions list
│ └── settings_screen.dart # Duration & locale settings
├── services/ # Business logic
│ ├── storage_service.dart # Hive database operations
│ ├── encouragement_service.dart # Random message picker
│ └── notification_service.dart # Background notifications
├── theme/ # Design system
│ ├── app_theme.dart # Material theme config
│ ├── app_colors.dart # Color palette constants
│ └── app_text_styles.dart # Typography definitions
└── l10n/ # Generated localization files
├── app_localizations.dart
└── app_localizations_*.dart
```
### Data Flow
**Focus Session Lifecycle**:
1. User selects duration in `HomeScreen` (loads from SharedPreferences)
2. `FocusScreen` starts countdown timer with `Timer.periodic`
3. User can tap "I got distracted" → adds to `_distractions` list, timer continues
4. Timer completion or manual stop → `FocusSession` saved to Hive via `StorageService`
5. `CompleteScreen` shows stats fetched from `StorageService.getTodaySessions()`
6. `HistoryScreen` displays all sessions from `StorageService.getAllSessions()`
**Critical Implementation**: The distraction button **never** pauses the timer - this is the app's core differentiator.
### FocusSession Model
```dart
@HiveType(typeId: 0)
class FocusSession {
DateTime startTime;
int durationMinutes; // Planned (e.g., 25)
int actualMinutes; // May be less if stopped early
int distractionCount; // Total distractions logged
bool completed; // True if timer reached zero
List<String> distractionTypes; // Categories selected
}
```
### Theme System
Uses Google Fonts (Nunito) with centralized design tokens:
- **Primary Color**: `#6C63FF` (Purple)
- **Background**: `#F5F7FA` (Light Gray)
- **Text Primary**: `#2D3748` (Dark Gray)
- **Success**: `#48BB78` (Green - used sparingly)
All UI components reference `AppTextStyles` and `AppColors` constants - never use hardcoded values.
## MVP Feature Set
**Current Implementation** (as of git commit `005ad8d`):
- ✅ Onboarding flow with "no-punishment" philosophy explanation
- ✅ Configurable session duration (15/25/45 minutes)
- ✅ Background notifications during active sessions
- ✅ Distraction tracking with 4 categories
- ✅ Today's stats on completion screen
- ✅ History view (today's sessions only in MVP)
- ✅ Multi-language support (13 languages)
- ✅ Settings for duration and locale
**Intentionally Deferred** (see [README.md](README.md)):
- ⏸️ Variable duration slider (fixed presets preferred for MVP)
- ⏸️ White noise audio playback
- ⏸️ Weekly trend charts
- ⏸️ PDF report export
- ⏸️ AdMob integration (monetization post-validation)
## Working with Localization
### Adding/Editing Translations
1. Translations are defined in `l10n/app_en.arb` (source locale) and other `app_*.arb` files
2. After editing ARB files, regenerate code:
```bash
flutter gen-l10n
```
3. Use in code:
```dart
final l10n = AppLocalizations.of(context)!;
Text(l10n.appTitle);
```
### Adding a New Language
1. Create `l10n/app_<locale>.arb` (copy from `app_en.arb`)
2. Add locale to `supportedLocales` in [main.dart](lib/main.dart:97-111)
3. Add to language picker in [settings_screen.dart](lib/screens/settings_screen.dart)
4. Run `flutter gen-l10n`
## Modifying Data Models
When changing Hive models (e.g., `FocusSession`):
1. Update model class with `@HiveField` annotations
2. Regenerate adapters:
```bash
flutter pub run build_runner build --delete-conflicting-outputs
```
3. Consider migration strategy if changing existing fields (current MVP has no migration logic)
## Code Generation
This project uses code generation for:
- **Hive adapters**: `*.g.dart` files for model serialization
- **Localization**: `lib/l10n/app_localizations*.dart` from ARB files
Never edit generated files manually - always modify source files and regenerate.
## Platform-Specific Notes
### Android
- Minimum SDK: 21 (Android 5.0)
- Target SDK: 34 (Android 14)
- Notification permission auto-requested on Android 13+ via `permission_handler`
### iOS
- Requires Xcode for building
- Notification permissions requested via `DarwinInitializationSettings`
- See [MACOS_BUILD_GUIDE.md](MACOS_BUILD_GUIDE.md) for macOS-specific instructions
### Web
- Notifications explicitly disabled on web platform (see `NotificationService._initialized`)
- Uses responsive layout but primarily designed for mobile
## Design System Guidelines
When adding new UI components:
1. Use `AppColors` constants - never hardcode colors
2. Use `AppTextStyles` for typography - never inline text styles
3. Border radius should be 16px for cards/buttons (consistency)
4. Button minimum height: 56px (touch-friendly)
5. Padding: Use multiples of 8 (8, 16, 24, 32, 48, 60)
6. Never use red/destructive colors for distractions (core philosophy)
## Testing Strategy
Current implementation focuses on manual testing:
- Test timer countdown accuracy across different durations
- Verify background notifications appear when app is minimized
- Test distraction button during active session (ensure timer continues)
- Verify data persists after app restart (Hive)
- Test locale switching without app restart
## Known Constraints
1. **No backend server** - All data stored locally (intentional for privacy)
2. **No analytics** - No Firebase/Amplitude in MVP (intentional)
3. **Single-device only** - No cross-device sync
4. **Today-focused** - History screen shows minimal data in MVP
## Important Implementation Rules
1. **Never pause timer on distraction** - This would violate the core philosophy
2. **Never show negative/punitive messaging** - Use encouragement instead
3. **Always use localized strings** - No hardcoded English text
4. **Always save sessions to Hive** - Even if stopped early (actualMinutes < durationMinutes)
5. **Background notifications required** - Users need countdown visibility when multitasking
## Related Documentation
- [README.md](README.md) - Product optimization strategy and MVP scope
- [ui-design-spec.md](ui-design-spec.md) - Complete UI/UX specifications
- [mvp-launch-checklist.md](mvp-launch-checklist.md) - 4-week development roadmap
- [product-design.md](product-design.md) - Original product vision
- [MACOS_BUILD_GUIDE.md](MACOS_BUILD_GUIDE.md) - macOS/iOS build instructions

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

@@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<!-- Simple timer/focus icon for notifications (white silhouette) -->
<!-- Circle representing focus/timer -->
<path
android:fillColor="#FFFFFFFF"
android:pathData="M12,2C6.48,2 2,6.48 2,12C2,17.52 6.48,22 12,22C17.52,22 22,17.52 22,12C22,6.48 17.52,2 12,2ZM12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20Z"/>
<!-- Clock hand pointing up (12 o'clock - focus start position) -->
<path
android:fillColor="#FFFFFFFF"
android:pathData="M11,7L11,12.41L14.29,15.71L15.71,14.29L13,11.59L13,7Z"/>
</vector>

Binary file not shown.

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

@@ -0,0 +1,70 @@
import 'package:flutter/material.dart';
import '../theme/app_colors.dart';
/// Control Buttons Component
class ControlButtons extends StatelessWidget {
final bool isPaused;
final VoidCallback onTogglePause;
final VoidCallback onStopEarly;
final String pauseText;
final String resumeText;
final String stopText;
const ControlButtons({
super.key,
required this.isPaused,
required this.onTogglePause,
required this.onStopEarly,
required this.pauseText,
required this.resumeText,
required this.stopText,
});
@override
Widget build(BuildContext context) {
return Column(
children: [
// Pause/Resume Button
SizedBox(
width: double.infinity,
child: OutlinedButton(
onPressed: onTogglePause,
style: OutlinedButton.styleFrom(
foregroundColor: AppColors.primary,
side: const BorderSide(color: AppColors.primary, width: 1),
minimumSize: const Size(double.infinity, 48),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(isPaused ? Icons.play_arrow : Icons.pause),
const SizedBox(width: 8),
Text(isPaused ? resumeText : pauseText),
],
),
),
),
const SizedBox(height: 16),
// Stop Button
Padding(
padding: const EdgeInsets.only(bottom: 24.0),
child: TextButton(
onPressed: onStopEarly,
child: Text(
stopText,
style: const TextStyle(
color: AppColors.textSecondary,
fontSize: 14,
),
),
),
),
],
);
}
}

View File

@@ -0,0 +1,51 @@
import 'package:flutter/material.dart';
import '../theme/app_colors.dart';
/// Distraction Button Component
class DistractionButton extends StatelessWidget {
final VoidCallback onPressed;
final String buttonText;
const DistractionButton({
super.key,
required this.onPressed,
required this.buttonText,
});
@override
Widget build(BuildContext context) {
return SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: onPressed,
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.distractionButton,
foregroundColor: AppColors.textPrimary,
minimumSize: const Size(double.infinity, 48),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
elevation: 0,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
buttonText,
style: const TextStyle(
fontFamily: 'Nunito',
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
const SizedBox(width: 8),
const Text(
'🤚',
style: TextStyle(fontSize: 20),
),
],
),
),
);
}
}

View File

@@ -0,0 +1,27 @@
import 'package:flutter/material.dart';
import '../theme/app_text_styles.dart';
/// Timer Display Component
class TimerDisplay extends StatelessWidget {
final int remainingSeconds;
const TimerDisplay({
super.key,
required this.remainingSeconds,
});
/// Format seconds to MM:SS format
String _formatTime(int seconds) {
final minutes = seconds ~/ 60;
final secs = seconds % 60;
return '${minutes.toString().padLeft(2, '0')}:${secs.toString().padLeft(2, '0')}';
}
@override
Widget build(BuildContext context) {
return Text(
_formatTime(remainingSeconds),
style: AppTextStyles.timerDisplay,
);
}
}

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

@@ -2,9 +2,8 @@ import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
import 'l10n/app_localizations.dart'; import 'l10n/app_localizations.dart';
import 'theme/app_theme.dart'; import 'theme/app_theme.dart';
import 'services/storage_service.dart'; import 'services/di.dart';
import 'services/encouragement_service.dart'; import 'services/encouragement_service.dart';
import 'services/notification_service.dart';
import 'screens/home_screen.dart'; import 'screens/home_screen.dart';
import 'screens/onboarding_screen.dart'; import 'screens/onboarding_screen.dart';
import 'screens/settings_screen.dart'; import 'screens/settings_screen.dart';
@@ -12,19 +11,10 @@ import 'screens/settings_screen.dart';
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
// Initialize services // Initialize dependency injection
await StorageService.init(); await initializeDI();
final encouragementService = EncouragementService(); runApp(MyApp(encouragementService: getIt<EncouragementService>()));
await encouragementService.loadMessages();
// Initialize notification service
final notificationService = NotificationService();
await notificationService.initialize();
// Request permissions on first launch
await notificationService.requestPermissions();
runApp(MyApp(encouragementService: encouragementService));
} }
class MyApp extends StatefulWidget { class MyApp extends StatefulWidget {

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

@@ -5,9 +5,15 @@ import '../theme/app_colors.dart';
import '../theme/app_text_styles.dart'; import '../theme/app_text_styles.dart';
import '../models/distraction_type.dart'; import '../models/distraction_type.dart';
import '../models/focus_session.dart'; import '../models/focus_session.dart';
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/distraction_button.dart';
import '../components/control_buttons.dart';
import 'complete_screen.dart'; import 'complete_screen.dart';
/// Focus Screen - Timer and distraction tracking /// Focus Screen - Timer and distraction tracking
@@ -32,7 +38,10 @@ class _FocusScreenState extends State<FocusScreen> with WidgetsBindingObserver {
final List<String> _distractions = []; final List<String> _distractions = [];
bool _isPaused = false; bool _isPaused = false;
bool _isInBackground = false; bool _isInBackground = false;
final NotificationService _notificationService = NotificationService(); final NotificationService _notificationService = getIt<NotificationService>();
final StorageService _storageService = getIt<StorageService>();
final PointsService _pointsService = getIt<PointsService>();
final AchievementService _achievementService = getIt<AchievementService>();
@override @override
void initState() { void initState() {
@@ -80,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,
@@ -102,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,
@@ -125,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();
_saveFocusSession(completed: true); // Calculate points and update user progress
final pointsData = await _saveFocusSession(completed: true);
if (!mounted) return; if (!mounted) return;
@@ -157,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,
), ),
), ),
@@ -178,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,
@@ -195,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),
), ),
@@ -218,22 +254,65 @@ class _FocusScreenState extends State<FocusScreen> with WidgetsBindingObserver {
); );
} }
Future<void> _saveFocusSession({required bool completed}) async { Future<Map<String, dynamic>> _saveFocusSession({
final actualMinutes = completed required bool completed,
? widget.durationMinutes }) async {
: ((widget.durationMinutes * 60 - _remainingSeconds) / 60).floor(); try {
final actualMinutes = completed
? widget.durationMinutes
: ((widget.durationMinutes * 60 - _remainingSeconds) / 60).floor();
final session = FocusSession( final session = FocusSession(
startTime: _startTime, startTime: _startTime,
durationMinutes: widget.durationMinutes, durationMinutes: widget.durationMinutes,
actualMinutes: actualMinutes, actualMinutes: actualMinutes,
distractionCount: _distractions.length, distractionCount: _distractions.length,
completed: completed, completed: completed,
distractionTypes: _distractions, distractionTypes: _distractions,
); );
final storageService = StorageService(); // 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) {
// Return default values on error
return {
'pointsEarned': 0,
'basePoints': 0,
'honestyBonus': 0,
'totalPoints': 0,
'newAchievements': <String>[],
};
}
} }
void _showDistractionSheet() { void _showDistractionSheet() {
@@ -241,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),
@@ -339,30 +421,26 @@ class _FocusScreenState extends State<FocusScreen> with WidgetsBindingObserver {
} }
void _recordDistraction(String? type) { void _recordDistraction(String? type) {
final l10n = AppLocalizations.of(context)!;
setState(() { setState(() {
if (type != null) { if (type != null) {
_distractions.add(type); _distractions.add(type);
} }
}); });
// Show encouragement toast // Show distraction-specific encouragement toast
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text(l10n.distractionEncouragement), content: Text(
widget.encouragementService.getRandomMessage(
EncouragementType.distraction,
),
),
duration: const Duration(seconds: 2), duration: const Duration(seconds: 2),
behavior: SnackBarBehavior.floating, behavior: SnackBarBehavior.floating,
), ),
); );
} }
String _formatTime(int seconds) {
final minutes = seconds ~/ 60;
final secs = seconds % 60;
return '${minutes.toString().padLeft(2, '0')}:${secs.toString().padLeft(2, '0')}';
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!; final l10n = AppLocalizations.of(context)!;
@@ -371,106 +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 const SizedBox(height: 80),
Text(
_formatTime(_remainingSeconds),
style: AppTextStyles.timerDisplay,
),
const SizedBox(height: 80), // "I got distracted" Button Component
DistractionButton(
// "I got distracted" Button onPressed: _showDistractionSheet,
SizedBox( buttonText: l10n.iGotDistracted,
width: double.infinity,
child: ElevatedButton(
onPressed: _showDistractionSheet,
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.distractionButton,
foregroundColor: AppColors.textPrimary,
minimumSize: const Size(double.infinity, 48),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
elevation: 0,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
l10n.iGotDistracted,
style: const TextStyle(
fontFamily: 'Nunito',
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
const SizedBox(width: 8),
const Text(
'🤚',
style: TextStyle(fontSize: 20),
),
],
),
),
),
const SizedBox(height: 16),
// Pause Button
SizedBox(
width: double.infinity,
child: OutlinedButton(
onPressed: _togglePause,
style: OutlinedButton.styleFrom(
foregroundColor: AppColors.primary,
side: const BorderSide(color: AppColors.primary, width: 1),
minimumSize: const Size(double.infinity, 48),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(_isPaused ? Icons.play_arrow : Icons.pause),
const SizedBox(width: 8),
Text(_isPaused ? l10n.resume : l10n.pause),
],
),
),
),
SizedBox(
height: MediaQuery.of(context).size.height * 0.2,
),
],
),
),
), ),
// Stop Button (text button at bottom) const SizedBox(height: 16),
Padding(
padding: const EdgeInsets.only(bottom: 24.0), // Control Buttons Component
child: TextButton( ControlButtons(
onPressed: _stopEarly, isPaused: _isPaused,
child: Text( onTogglePause: _togglePause,
l10n.stopSession, onStopEarly: _stopEarly,
style: const TextStyle( pauseText: l10n.pause,
color: AppColors.textSecondary, resumeText: l10n.resume,
fontSize: 14, stopText: l10n.stopSession,
),
),
),
), ),
], ],
), ),

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() {
@@ -30,15 +34,23 @@ class _HomeScreenState extends State<HomeScreen> {
} }
Future<void> _loadDefaultDuration() async { Future<void> _loadDefaultDuration() async {
final duration = await SettingsScreen.getDefaultDuration(); try {
setState(() { final duration = await SettingsScreen.getDefaultDuration();
_defaultDuration = duration; setState(() {
}); _defaultDuration = duration;
});
} catch (e) {
// Use default duration if loading fails
setState(() {
_defaultDuration = 25;
});
}
} }
@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,
@@ -46,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,
@@ -93,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(
@@ -161,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

@@ -11,20 +11,34 @@ class SettingsScreen extends StatefulWidget {
/// Get the saved default duration (for use in other screens) /// Get the saved default duration (for use in other screens)
static Future<int> getDefaultDuration() async { static Future<int> getDefaultDuration() async {
final prefs = await SharedPreferences.getInstance(); try {
return prefs.getInt(_durationKey) ?? 25; final prefs = await SharedPreferences.getInstance();
return prefs.getInt(_durationKey) ?? 25;
} catch (e) {
// Return default duration if loading fails
return 25;
}
} }
/// Get the saved locale /// Get the saved locale
static Future<String?> getSavedLocale() async { static Future<String?> getSavedLocale() async {
final prefs = await SharedPreferences.getInstance(); try {
return prefs.getString(_localeKey); final prefs = await SharedPreferences.getInstance();
return prefs.getString(_localeKey);
} catch (e) {
// Return null if loading fails
return null;
}
} }
/// Save the locale /// Save the locale
static Future<void> saveLocale(String localeCode) async { static Future<void> saveLocale(String localeCode) async {
final prefs = await SharedPreferences.getInstance(); try {
await prefs.setString(_localeKey, localeCode); final prefs = await SharedPreferences.getInstance();
await prefs.setString(_localeKey, localeCode);
} catch (e) {
// Ignore save errors
}
} }
static const String _durationKey = 'default_duration'; static const String _durationKey = 'default_duration';
@@ -48,36 +62,58 @@ class _SettingsScreenState extends State<SettingsScreen> {
} }
Future<void> _loadSavedDuration() async { Future<void> _loadSavedDuration() async {
final prefs = await SharedPreferences.getInstance(); try {
setState(() { final prefs = await SharedPreferences.getInstance();
_selectedDuration = prefs.getInt(SettingsScreen._durationKey) ?? 25; setState(() {
}); _selectedDuration = prefs.getInt(SettingsScreen._durationKey) ?? 25;
});
} catch (e) {
// Use default duration if loading fails
setState(() {
_selectedDuration = 25;
});
}
} }
Future<void> _loadSavedLocale() async { Future<void> _loadSavedLocale() async {
final prefs = await SharedPreferences.getInstance(); try {
setState(() { final prefs = await SharedPreferences.getInstance();
_selectedLocale = prefs.getString(SettingsScreen._localeKey) ?? 'en'; setState(() {
}); _selectedLocale = prefs.getString(SettingsScreen._localeKey) ?? 'en';
});
} catch (e) {
// Use default locale if loading fails
setState(() {
_selectedLocale = 'en';
});
}
} }
Future<void> _saveDuration(int duration) async { Future<void> _saveDuration(int duration) async {
final prefs = await SharedPreferences.getInstance(); try {
await prefs.setInt(SettingsScreen._durationKey, duration); final prefs = await SharedPreferences.getInstance();
setState(() { await prefs.setInt(SettingsScreen._durationKey, duration);
_selectedDuration = duration; setState(() {
}); _selectedDuration = duration;
});
} catch (e) {
// Ignore save errors, state will be reset on next load
}
} }
Future<void> _saveLocale(String localeCode) async { Future<void> _saveLocale(String localeCode) async {
await SettingsScreen.saveLocale(localeCode); try {
setState(() { await SettingsScreen.saveLocale(localeCode);
_selectedLocale = localeCode; setState(() {
}); _selectedLocale = localeCode;
});
// Update locale immediately without restart // Update locale immediately without restart
if (!mounted) return; if (!mounted) return;
MyApp.updateLocale(context, localeCode); MyApp.updateLocale(context, localeCode);
} catch (e) {
// Ignore save errors
}
} }
@override @override

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();
}
}

57
lib/services/di.dart Normal file
View File

@@ -0,0 +1,57 @@
import 'package:get_it/get_it.dart';
import 'package:flutter/foundation.dart';
import 'storage_service.dart';
import 'notification_service.dart';
import 'encouragement_service.dart';
import 'points_service.dart';
import 'achievement_service.dart';
/// GetIt instance for dependency injection
final getIt = GetIt.instance;
/// Initialize dependency injection
Future<void> initializeDI() async {
try {
// Register services as singletons
getIt.registerSingletonAsync<StorageService>(() async {
final service = StorageService();
await service.init();
return service;
});
getIt.registerSingletonAsync<NotificationService>(() async {
final service = NotificationService();
await service.initialize();
await service.requestPermissions();
return service;
});
getIt.registerSingletonAsync<EncouragementService>(() async {
final service = EncouragementService();
await service.loadMessages();
return service;
});
// Register synchronous services
getIt.registerSingleton<PointsService>(PointsService());
getIt.registerSingleton<AchievementService>(AchievementService());
// Wait for all services to be initialized
await getIt.allReady();
if (kDebugMode) {
print('Dependency injection initialized successfully');
}
} catch (e) {
if (kDebugMode) {
print('Failed to initialize dependency injection: $e');
}
rethrow;
}
}
/// Reset dependency injection (for testing)
void resetDI() {
getIt.reset();
}

View File

@@ -2,38 +2,154 @@ import 'dart:convert';
import 'dart:math'; import 'dart:math';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
/// Service to manage encouragement messages /// Enum representing different encouragement message types
enum EncouragementType {
general, // General encouragement messages
start, // When starting a focus session
distraction, // When user gets distracted
complete, // When completing a focus session
earlyStop, // When stopping early
}
/// Service to manage encouragement messages for different scenarios
class EncouragementService { class EncouragementService {
List<String> _messages = []; // Map of encouragement types to their messages
final Map<EncouragementType, List<String>> _messages = {
EncouragementType.general: [],
EncouragementType.start: [],
EncouragementType.distraction: [],
EncouragementType.complete: [],
EncouragementType.earlyStop: [],
};
final Random _random = Random(); final Random _random = Random();
/// Load encouragement messages from assets /// Load encouragement messages from assets
Future<void> loadMessages() async { Future<void> loadMessages() async {
try { try {
final String jsonString = final String jsonString =
await rootBundle.loadString('assets/encouragements.json'); await rootBundle.loadString('assets/encouragements.json');
final List<dynamic> jsonList = json.decode(jsonString); final dynamic jsonData = json.decode(jsonString);
_messages = jsonList.cast<String>();
// Check if the JSON is a map (new format with categories)
if (jsonData is Map<String, dynamic>) {
// Load categorized messages
_loadCategorizedMessages(jsonData);
} else if (jsonData is List<dynamic>) {
// Load legacy format (list of general messages)
_messages[EncouragementType.general] = jsonData.cast<String>();
// Initialize other categories with default messages
_initializeDefaultMessages();
} else {
// Invalid format, use defaults
_initializeDefaultMessages();
}
} catch (e) { } catch (e) {
// Fallback messages if file can't be loaded // Fallback to default messages if file can't be loaded
_messages = [ _initializeDefaultMessages();
"Showing up is half the battle.", }
"Every minute counts.", }
"You're learning, not failing.",
"Gentleness is strength.", /// Load categorized messages from JSON map
"Progress over perfection.", void _loadCategorizedMessages(Map<String, dynamic> jsonData) {
]; // Load general messages
if (jsonData.containsKey('general') && jsonData['general'] is List) {
_messages[EncouragementType.general] = (jsonData['general'] as List).cast<String>();
}
// Load start messages
if (jsonData.containsKey('start') && jsonData['start'] is List) {
_messages[EncouragementType.start] = (jsonData['start'] as List).cast<String>();
}
// Load distraction messages
if (jsonData.containsKey('distraction') && jsonData['distraction'] is List) {
_messages[EncouragementType.distraction] = (jsonData['distraction'] as List).cast<String>();
}
// Load complete messages
if (jsonData.containsKey('complete') && jsonData['complete'] is List) {
_messages[EncouragementType.complete] = (jsonData['complete'] as List).cast<String>();
}
// Load early stop messages
if (jsonData.containsKey('earlyStop') && jsonData['earlyStop'] is List) {
_messages[EncouragementType.earlyStop] = (jsonData['earlyStop'] as List).cast<String>();
}
// Ensure all categories have at least some messages
_ensureAllCategoriesHaveMessages();
}
/// Initialize default messages for all categories
void _initializeDefaultMessages() {
_messages[EncouragementType.general] = [
"Showing up is half the battle.",
"Every minute counts.",
"You're learning, not failing.",
"Gentleness is strength.",
"Progress over perfection.",
];
_messages[EncouragementType.start] = [
"You've got this! Let's begin.",
"Ready to focus? Let's do this.",
"Every moment is a fresh start.",
"Let's make this session count.",
"You're already making progress by showing up.",
];
_messages[EncouragementType.distraction] = [
"It's okay to get distracted. Let's gently come back.",
"No guilt here! Let's try again.",
"Distractions happen to everyone. Let's refocus.",
"You're doing great by noticing and coming back.",
"Gentle reminder: you can always start again.",
];
_messages[EncouragementType.complete] = [
"🎉 Congratulations! You did it!",
"Great job completing your focus session!",
"You should be proud of yourself!",
"That was amazing! Well done.",
"You're making wonderful progress!",
];
_messages[EncouragementType.earlyStop] = [
"It's okay to stop early. You tried, and that's what matters.",
"Every effort counts, even if it's shorter than planned.",
"You did your best, and that's enough.",
"Rest when you need to. We'll be here when you're ready.",
"Progress, not perfection. You're doing great.",
];
}
/// Ensure all categories have at least some messages
void _ensureAllCategoriesHaveMessages() {
// If any category is empty, use general messages as fallback
for (final type in EncouragementType.values) {
if (_messages[type]?.isEmpty ?? true) {
_messages[type] = List.from(_messages[EncouragementType.general]!);
}
} }
} }
/// Get a random encouragement message /// Get a random encouragement message for a specific type
String getRandomMessage() { String getRandomMessage([EncouragementType type = EncouragementType.general]) {
if (_messages.isEmpty) { final messages = _messages[type] ?? [];
if (messages.isEmpty) {
return "You're doing great!"; return "You're doing great!";
} }
return _messages[_random.nextInt(_messages.length)]; return messages[_random.nextInt(messages.length)];
} }
/// Get all messages (for testing) /// Get all messages for a specific type (for testing)
List<String> getAllMessages() => List.from(_messages); List<String> getAllMessages([EncouragementType type = EncouragementType.general]) {
return List.from(_messages[type] ?? []);
}
/// Get all messages for all types (for testing)
Map<EncouragementType, List<String>> getAllMessagesByType() {
return Map.from(_messages);
}
} }

View File

@@ -1,3 +1,4 @@
import 'dart:async';
import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
@@ -9,10 +10,21 @@ class NotificationService {
factory NotificationService() => _instance; factory NotificationService() => _instance;
NotificationService._internal(); NotificationService._internal();
final FlutterLocalNotificationsPlugin _notifications = final FlutterLocalNotificationsPlugin _notifications =
FlutterLocalNotificationsPlugin(); FlutterLocalNotificationsPlugin();
bool _initialized = false; bool _initialized = false;
/// Stream controller for permission status changes
final StreamController<bool> _permissionStatusController = StreamController<bool>.broadcast();
/// Get the permission status stream
Stream<bool> get permissionStatusStream => _permissionStatusController.stream;
/// Dispose the stream controller
void dispose() {
_permissionStatusController.close();
}
/// Initialize notification service /// Initialize notification service
Future<void> initialize() async { Future<void> initialize() async {
@@ -28,7 +40,9 @@ class NotificationService {
try { try {
// Android initialization settings // Android initialization settings
const androidSettings = AndroidInitializationSettings('@mipmap/ic_launcher'); const androidSettings = AndroidInitializationSettings(
'@drawable/ic_notification',
);
// iOS initialization settings // iOS initialization settings
const iosSettings = DarwinInitializationSettings( const iosSettings = DarwinInitializationSettings(
@@ -48,6 +62,13 @@ class NotificationService {
); );
_initialized = true; _initialized = true;
// Start listening for permission changes
await listenForPermissionChanges();
// Check initial permission status
await hasPermission();
if (kDebugMode) { if (kDebugMode) {
print('Notification service initialized successfully'); print('Notification service initialized successfully');
} }
@@ -63,7 +84,6 @@ class NotificationService {
if (kDebugMode) { if (kDebugMode) {
print('Notification tapped: ${response.payload}'); print('Notification tapped: ${response.payload}');
} }
// TODO: Navigate to appropriate screen if needed
} }
/// Request notification permissions (iOS and Android 13+) /// Request notification permissions (iOS and Android 13+)
@@ -71,39 +91,43 @@ class NotificationService {
if (kIsWeb) return false; if (kIsWeb) return false;
try { try {
bool isGranted = false;
// Check if we're on Android or iOS // Check if we're on Android or iOS
if (Platform.isAndroid) { if (Platform.isAndroid) {
// Android 13+ requires runtime permission // Android 13+ requires runtime permission
final status = await Permission.notification.request(); final status = await Permission.notification.request();
isGranted = status.isGranted;
if (kDebugMode) { if (kDebugMode) {
print('Android notification permission status: $status'); print('Android notification permission status: $status');
} }
return status.isGranted;
} else if (Platform.isIOS) { } else if (Platform.isIOS) {
// iOS permission request // iOS permission request
final result = await _notifications final iosImplementation = _notifications.resolvePlatformSpecificImplementation<IOSFlutterLocalNotificationsPlugin>();
.resolvePlatformSpecificImplementation< if (iosImplementation != null) {
IOSFlutterLocalNotificationsPlugin>() final result = await iosImplementation.requestPermissions(alert: true, badge: true, sound: true);
?.requestPermissions( isGranted = result ?? false;
alert: true,
badge: true, if (kDebugMode) {
sound: true, print('iOS notification permission result: $result');
); }
} else {
if (kDebugMode) { isGranted = true; // Assume granted if we can't request
print('iOS notification permission result: $result');
} }
} else {
return result ?? false; isGranted = true; // Assume granted for other platforms
} }
return true; // Other platforms // Update the permission status stream
_permissionStatusController.add(isGranted);
return isGranted;
} catch (e) { } catch (e) {
if (kDebugMode) { if (kDebugMode) {
print('Failed to request permissions: $e'); print('Failed to request permissions: $e');
} }
_permissionStatusController.add(false);
return false; return false;
} }
} }
@@ -113,21 +137,39 @@ class NotificationService {
if (kIsWeb) return false; if (kIsWeb) return false;
try { try {
bool isGranted = false;
if (Platform.isAndroid) { if (Platform.isAndroid) {
final status = await Permission.notification.status; final status = await Permission.notification.status;
return status.isGranted; isGranted = status.isGranted;
} else if (Platform.isIOS) { } else if (Platform.isIOS) {
// For iOS, we can't easily check without requesting, so we assume granted after request // For iOS, we assume granted after initial request
return true; isGranted = true;
} else {
isGranted = true; // Assume granted for other platforms
} }
return true;
// Update the permission status stream
_permissionStatusController.add(isGranted);
return isGranted;
} catch (e) { } catch (e) {
if (kDebugMode) { if (kDebugMode) {
print('Failed to check permission status: $e'); print('Failed to check permission status: $e');
} }
_permissionStatusController.add(false);
return false; return false;
} }
} }
/// Listen for permission status changes
Future<void> listenForPermissionChanges() async {
// Permission status changes listening is not supported in current permission_handler version
// This method is kept for future implementation
if (kDebugMode) {
print('Permission status changes listening is not supported');
}
}
/// Show focus session completed notification /// Show focus session completed notification
Future<void> showFocusCompletedNotification({ Future<void> showFocusCompletedNotification({
@@ -147,6 +189,7 @@ class NotificationService {
priority: Priority.high, priority: Priority.high,
enableVibration: true, enableVibration: true,
playSound: true, playSound: true,
icon: '@drawable/ic_notification',
); );
const iosDetails = DarwinNotificationDetails( const iosDetails = DarwinNotificationDetails(
@@ -162,7 +205,8 @@ class NotificationService {
// Use provided title/body or fall back to English // Use provided title/body or fall back to English
final notificationTitle = title ?? '🎉 Focus session complete!'; final notificationTitle = title ?? '🎉 Focus session complete!';
final notificationBody = body ?? final notificationBody =
body ??
(distractionCount == 0 (distractionCount == 0
? 'You focused for $minutes ${minutes == 1 ? 'minute' : 'minutes'} without distractions!' ? 'You focused for $minutes ${minutes == 1 ? 'minute' : 'minutes'} without distractions!'
: 'You focused for $minutes ${minutes == 1 ? 'minute' : 'minutes'}. Great effort!'); : 'You focused for $minutes ${minutes == 1 ? 'minute' : 'minutes'}. Great effort!');
@@ -186,9 +230,7 @@ class NotificationService {
} }
/// Show reminder notification (optional feature for future) /// Show reminder notification (optional feature for future)
Future<void> showReminderNotification({ Future<void> showReminderNotification({required String message}) async {
required String message,
}) async {
if (kIsWeb || !_initialized) return; if (kIsWeb || !_initialized) return;
try { try {
@@ -198,6 +240,7 @@ class NotificationService {
channelDescription: 'Gentle reminders to focus', channelDescription: 'Gentle reminders to focus',
importance: Importance.defaultImportance, importance: Importance.defaultImportance,
priority: Priority.defaultPriority, priority: Priority.defaultPriority,
icon: '@drawable/ic_notification',
); );
const iosDetails = DarwinNotificationDetails(); const iosDetails = DarwinNotificationDetails();
@@ -259,7 +302,8 @@ class NotificationService {
try { try {
// Format time display for fallback // Format time display for fallback
final timeStr = '${remainingMinutes.toString().padLeft(2, '0')}:${(remainingSeconds % 60).toString().padLeft(2, '0')}'; final timeStr =
'${remainingMinutes.toString().padLeft(2, '0')}:${(remainingSeconds % 60).toString().padLeft(2, '0')}';
const androidDetails = AndroidNotificationDetails( const androidDetails = AndroidNotificationDetails(
'focus_timer', 'focus_timer',
@@ -274,6 +318,7 @@ class NotificationService {
playSound: false, playSound: false,
// Show in status bar // Show in status bar
showProgress: false, showProgress: false,
icon: '@drawable/ic_notification',
); );
const iosDetails = DarwinNotificationDetails( const iosDetails = DarwinNotificationDetails(

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

@@ -0,0 +1,99 @@
import 'package:flutter/foundation.dart';
import 'storage_service.dart';
import 'notification_service.dart';
import 'encouragement_service.dart';
import 'points_service.dart';
import 'achievement_service.dart';
/// Service Locator - 统一管理所有服务实例
class ServiceLocator {
static final ServiceLocator _instance = ServiceLocator._internal();
factory ServiceLocator() => _instance;
ServiceLocator._internal();
late StorageService _storageService;
late NotificationService _notificationService;
late EncouragementService _encouragementService;
late PointsService _pointsService;
late AchievementService _achievementService;
bool _isInitialized = false;
/// 初始化所有服务
Future<void> initialize() async {
if (_isInitialized) return;
try {
// 初始化存储服务
_storageService = StorageService();
await _storageService.init();
// 初始化通知服务
_notificationService = NotificationService();
await _notificationService.initialize();
await _notificationService.requestPermissions();
// 初始化鼓励语服务
_encouragementService = EncouragementService();
await _encouragementService.loadMessages();
// 初始化积分服务
_pointsService = PointsService();
// 初始化成就服务
_achievementService = AchievementService();
_isInitialized = true;
if (kDebugMode) {
print('ServiceLocator initialized successfully');
}
} catch (e) {
if (kDebugMode) {
print('Failed to initialize ServiceLocator: $e');
}
rethrow;
}
}
/// 获取存储服务实例
StorageService get storageService {
_checkInitialized();
return _storageService;
}
/// 获取通知服务实例
NotificationService get notificationService {
_checkInitialized();
return _notificationService;
}
/// 获取鼓励语服务实例
EncouragementService get encouragementService {
_checkInitialized();
return _encouragementService;
}
/// 获取积分服务实例
PointsService get pointsService {
_checkInitialized();
return _pointsService;
}
/// 获取成就服务实例
AchievementService get achievementService {
_checkInitialized();
return _achievementService;
}
/// 检查服务是否已初始化
void _checkInitialized() {
if (!_isInitialized) {
throw Exception('ServiceLocator has not been initialized yet. Call initialize() first.');
}
}
/// 重置服务(用于测试)
void reset() {
_isInitialized = false;
}
}

View File

@@ -1,62 +1,222 @@
import 'package:hive_flutter/hive_flutter.dart'; import 'package:hive_flutter/hive_flutter.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';
/// Initialize Hive // Cache for today's sessions to improve performance
static Future<void> init() async { List<FocusSession>? _todaySessionsCache;
await Hive.initFlutter(); DateTime? _cacheDate;
// Register adapters // Cache for user progress
Hive.registerAdapter(FocusSessionAdapter()); UserProgress? _userProgressCache;
// Open boxes /// Initialize Hive storage service
await Hive.openBox<FocusSession>(_focusSessionBox); ///
/// This method initializes Hive, registers adapters, and opens the focus sessions box.
/// It should be called once during app initialization.
Future<void> init() async {
try {
await Hive.initFlutter();
// Register adapters
Hive.registerAdapter(FocusSessionAdapter());
Hive.registerAdapter(UserProgressAdapter());
// Open boxes
await Hive.openBox<FocusSession>(_focusSessionBox);
await Hive.openBox<UserProgress>(_userProgressBox);
if (kDebugMode) {
print('StorageService initialized successfully');
}
} catch (e) {
if (kDebugMode) {
print('Failed to initialize StorageService: $e');
}
rethrow;
}
} }
/// 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);
/// Save a focus session /// Get the user progress box
Box<UserProgress> get _progressBox => Hive.box<UserProgress>(_userProgressBox);
/// Invalidate the cache when data changes
void _invalidateCache() {
_todaySessionsCache = 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
///
/// [session] - The focus session to save
/// Returns a Future that completes when the session is saved
Future<void> saveFocusSession(FocusSession session) async { Future<void> saveFocusSession(FocusSession session) async {
await _sessionsBox.add(session); try {
await _sessionsBox.add(session);
_invalidateCache(); // Invalidate cache when data changes
} catch (e) {
if (kDebugMode) {
print('Failed to save focus session: $e');
}
rethrow;
}
} }
/// Get all focus sessions /// Get all focus sessions from local storage
///
/// Returns a list of all focus sessions stored locally
List<FocusSession> getAllSessions() { List<FocusSession> getAllSessions() {
return _sessionsBox.values.toList(); try {
return _sessionsBox.values.toList();
} catch (e) {
if (kDebugMode) {
print('Failed to get all sessions: $e');
}
return [];
}
} }
/// Get today's focus sessions /// Get today's focus sessions with caching
///
/// Returns a list of focus sessions that occurred today
/// Uses caching to improve performance for frequent calls
List<FocusSession> getTodaySessions() { List<FocusSession> getTodaySessions() {
final now = DateTime.now(); try {
final today = DateTime(now.year, now.month, now.day); final now = DateTime.now();
final today = DateTime(now.year, now.month, now.day);
return _sessionsBox.values.where((session) { // Check if cache is valid
final sessionDate = DateTime( if (_todaySessionsCache != null && _cacheDate == today) {
session.startTime.year, return _todaySessionsCache!;
session.startTime.month, }
session.startTime.day,
); // Query and cache results
return sessionDate == today; final sessions = _sessionsBox.values.where((session) {
}).toList(); final sessionDate = DateTime(
session.startTime.year,
session.startTime.month,
session.startTime.day,
);
return sessionDate == today;
}).toList();
// Update cache
_todaySessionsCache = sessions;
_cacheDate = today;
return sessions;
} catch (e) {
if (kDebugMode) {
print('Failed to get today\'s sessions: $e');
}
return [];
}
} }
/// Get total focus minutes for today /// Get total focus minutes for today
///
/// Returns the sum of actual minutes focused today
int getTodayTotalMinutes() { int getTodayTotalMinutes() {
return getTodaySessions() return getTodaySessions()
.fold<int>(0, (sum, session) => sum + session.actualMinutes); .fold<int>(0, (sum, session) => sum + session.actualMinutes);
} }
/// Get total distractions for today /// Get total distractions for today
///
/// Returns the total number of distractions recorded today
int getTodayDistractionCount() { int getTodayDistractionCount() {
return getTodaySessions() return getTodaySessions()
.fold<int>(0, (sum, session) => sum + session.distractionCount); .fold<int>(0, (sum, session) => sum + session.distractionCount);
} }
/// Get total completed sessions for today /// Get total completed sessions for today
///
/// Returns the number of focus sessions completed today
int getTodayCompletedCount() { int getTodayCompletedCount() {
return getTodaySessions() return getTodaySessions()
.where((session) => session.completed) .where((session) => session.completed)
@@ -64,22 +224,54 @@ class StorageService {
} }
/// Get total sessions count for today (including stopped early) /// Get total sessions count for today (including stopped early)
///
/// Returns the total number of focus sessions started today
int getTodaySessionsCount() { int getTodaySessionsCount() {
return getTodaySessions().length; return getTodaySessions().length;
} }
/// Delete a focus session /// Delete a focus session from local storage
///
/// [session] - The focus session to delete
/// Returns a Future that completes when the session is deleted
Future<void> deleteSession(FocusSession session) async { Future<void> deleteSession(FocusSession session) async {
await session.delete(); try {
await session.delete();
_invalidateCache(); // Invalidate cache when data changes
} catch (e) {
if (kDebugMode) {
print('Failed to delete focus session: $e');
}
rethrow;
}
} }
/// Clear all sessions (for testing/debugging) /// Clear all sessions from local storage (for testing/debugging)
///
/// Returns a Future that completes when all sessions are cleared
Future<void> clearAllSessions() async { Future<void> clearAllSessions() async {
await _sessionsBox.clear(); try {
await _sessionsBox.clear();
_invalidateCache(); // Invalidate cache when data changes
} catch (e) {
if (kDebugMode) {
print('Failed to clear all sessions: $e');
}
rethrow;
}
} }
/// Close all boxes /// Close all Hive boxes
///
/// Should be called when the app is closing to properly clean up resources
static Future<void> close() async { static Future<void> close() async {
await Hive.close(); try {
await Hive.close();
} catch (e) {
if (kDebugMode) {
print('Failed to close Hive boxes: $e');
}
rethrow;
}
} }
} }

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

@@ -277,6 +277,14 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "4.0.0" version: "4.0.0"
get_it:
dependency: "direct main"
description:
name: get_it
sha256: d85128a5dae4ea777324730dc65edd9c9f43155c109d5cc0a69cab74139fbac1
url: "https://pub.flutter-io.cn"
source: hosted
version: "7.7.0"
glob: glob:
dependency: transitive dependency: transitive
description: description:

View File

@@ -27,25 +27,26 @@ environment:
# dependencies can be manually updated by changing the version numbers below to # dependencies can be manually updated by changing the version numbers below to
# the latest version available on pub.dev. To see which dependencies have newer # the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`. # versions available, run `flutter pub outdated`.
dependencies: dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
flutter_localizations: flutter_localizations:
sdk: flutter sdk: flutter
# The following adds the Cupertino Icons font to your application. # The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons. # Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.8 cupertino_icons: ^1.0.8
# MVP Required Dependencies # MVP Required Dependencies
hive: ^2.2.3 # Local storage hive: ^2.2.3 # Local storage
hive_flutter: ^1.1.0 # Hive Flutter integration hive_flutter: ^1.1.0 # Hive Flutter integration
flutter_local_notifications: ^17.0.0 # Notifications flutter_local_notifications: ^17.0.0 # Notifications
permission_handler: ^11.0.0 # Runtime permissions (Android 13+) permission_handler: ^11.0.0 # Runtime permissions (Android 13+)
path_provider: ^2.1.0 # File paths path_provider: ^2.1.0 # File paths
shared_preferences: ^2.2.0 # Simple key-value storage (for onboarding) shared_preferences: ^2.2.0 # Simple key-value storage (for onboarding)
intl: ^0.20.2 # Date formatting and i18n intl: ^0.20.2 # Date formatting and i18n
google_fonts: ^6.1.0 # Google Fonts (Nunito) google_fonts: ^6.1.0 # Google Fonts (Nunito)
get_it: ^7.7.0 # Dependency injection framework
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

View File

@@ -0,0 +1,79 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:focus_buddy/services/encouragement_service.dart';
void main() {
group('EncouragementService', () {
late EncouragementService encouragementService;
setUp(() {
encouragementService = EncouragementService();
});
test('should initialize with default messages when load fails', () async {
// Act: Try to load messages (will fail since we're in test environment)
await encouragementService.loadMessages();
// Assert: Should have default messages for all types
expect(encouragementService.getAllMessages(), isNotEmpty);
expect(encouragementService.getAllMessages(EncouragementType.start), isNotEmpty);
expect(encouragementService.getAllMessages(EncouragementType.distraction), isNotEmpty);
expect(encouragementService.getAllMessages(EncouragementType.complete), isNotEmpty);
expect(encouragementService.getAllMessages(EncouragementType.earlyStop), isNotEmpty);
});
test('should return a random message for each type', () async {
// Arrange: Load messages
await encouragementService.loadMessages();
// Act: Get random messages for each type
final generalMessage = encouragementService.getRandomMessage();
final startMessage = encouragementService.getRandomMessage(EncouragementType.start);
final distractionMessage = encouragementService.getRandomMessage(EncouragementType.distraction);
final completeMessage = encouragementService.getRandomMessage(EncouragementType.complete);
final earlyStopMessage = encouragementService.getRandomMessage(EncouragementType.earlyStop);
// Assert: All messages should be non-empty strings
expect(generalMessage, isNotEmpty);
expect(startMessage, isNotEmpty);
expect(distractionMessage, isNotEmpty);
expect(completeMessage, isNotEmpty);
expect(earlyStopMessage, isNotEmpty);
});
test('should return general messages when using default type', () async {
// Arrange: Load messages
await encouragementService.loadMessages();
final generalMessages = encouragementService.getAllMessages();
// Act: Get a random message with default type
final message = encouragementService.getRandomMessage();
// Assert: Message should be in the general messages list
expect(generalMessages, contains(message));
});
test('should return distraction-specific messages when requested', () async {
// Arrange: Load messages
await encouragementService.loadMessages();
final distractionMessages = encouragementService.getAllMessages(EncouragementType.distraction);
// Act: Get a random distraction message
final message = encouragementService.getRandomMessage(EncouragementType.distraction);
// Assert: Message should be in the distraction messages list
expect(distractionMessages, contains(message));
});
test('should return complete-specific messages when requested', () async {
// Arrange: Load messages
await encouragementService.loadMessages();
final completeMessages = encouragementService.getAllMessages(EncouragementType.complete);
// Act: Get a random complete message
final message = encouragementService.getRandomMessage(EncouragementType.complete);
// Assert: Message should be in the complete messages list
expect(completeMessages, contains(message));
});
});
}

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:** 上架应用商店