Compare commits
8 Commits
005ad8ddf2
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
86a368e1e3 | ||
|
|
ef44d11c32 | ||
|
|
15252dfd88 | ||
|
|
5dccf27059 | ||
|
|
58f6ec39b7 | ||
|
|
0195cdf54b | ||
|
|
96658339e1 | ||
|
|
f8c4a18920 |
@@ -43,7 +43,26 @@
|
||||
"Bash(flutter clean:*)",
|
||||
"Bash(start ms-settings:developers)",
|
||||
"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": [],
|
||||
"ask": []
|
||||
|
||||
208
.trae/documents/FocusBuddy 代码优化计划.md
Normal file
208
.trae/documents/FocusBuddy 代码优化计划.md
Normal 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
284
CLAUDE.md
Normal 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
|
||||
@@ -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
362
README.md
@@ -1,174 +1,131 @@
|
||||
# FocusBuddy 产品优化总结
|
||||
# FocusBuddy 产品实现总结
|
||||
|
||||
**优化日期**: 2025年11月22日
|
||||
**目标**: 打造一个 4 周内可上线的 MVP 版本
|
||||
**策略**: 删繁就简,聚焦核心价值
|
||||
**实现日期**: 2025年11月27日
|
||||
**状态**: 已完成 MVP 版本开发
|
||||
**核心价值**: 无惩罚专注,温柔回归
|
||||
|
||||
---
|
||||
|
||||
## 📂 新增文档清单
|
||||
|
||||
已为你创建以下完整的产品文档:
|
||||
## 📂 文档清单
|
||||
|
||||
| 文档 | 路径 | 用途 |
|
||||
|------|------|------|
|
||||
| ✅ 产品设计 | [product-design.md](product-design.md) | 原始完整方案 |
|
||||
| ✅ UI 设计规范 | [ui-design-spec.md](ui-design-spec.md) | 完整的 UI/UX 细节(已补全) |
|
||||
| ✅ 隐私政策 | [privacy-policy.md](privacy-policy.md) | 需填写开发者信息 |
|
||||
| ✅ **MVP 上线清单** | [mvp-launch-checklist.md](mvp-launch-checklist.md) | **核心文档!必读** |
|
||||
| ✅ **应用商店文案** | [app-store-metadata.md](app-store-metadata.md) | 上架时直接复制使用 |
|
||||
| ✅ 产品设计 | [product-design.md](product-design.md) | 产品理念和市场定位 |
|
||||
| ✅ UI 设计规范 | [ui-design-spec.md](ui-design-spec.md) | UI/UX 设计细节 |
|
||||
| ✅ 隐私政策 | [privacy-policy.md](privacy-policy.md) | 隐私保护声明 |
|
||||
| ✅ 应用商店文案 | [app-store-metadata.md](app-store-metadata.md) | 上架时直接复制使用 |
|
||||
| ✅ 服务条款 | [terms-of-service.md](terms-of-service.md) | 上架必须项 |
|
||||
|
||||
---
|
||||
|
||||
## 🎯 核心优化建议汇总
|
||||
## 🎯 已实现核心功能
|
||||
|
||||
### 1. 功能精简(最重要)
|
||||
### 1. 页面功能
|
||||
|
||||
#### 从原方案删除/延后的功能:
|
||||
| 页面 | 功能 | 说明 |
|
||||
|------|------|------|
|
||||
| **Home** | 一键开始专注 | 显示积分卡片、应用标题、时长选择、开始专注按钮和底部导航 |
|
||||
| **Focus** | 专注计时 | 显示计时器、分心按钮和暂停按钮 |
|
||||
| **Complete** | 专注完成 | 显示专注结果、鼓励文案和"Start Another"按钮 |
|
||||
| **History** | 历史记录 | 显示当天记录列表,支持查看详情 |
|
||||
| **Settings** | 设置选项 | 包含默认时长选项、语言选择和隐私政策链接 |
|
||||
| **Profile** | 个人资料 | 显示积分、等级和连续签到记录 |
|
||||
| **Onboarding** | 引导页 | 解释"无惩罚"理念,降低用户困惑 |
|
||||
|
||||
| 原功能 | 决策 | 原因 |
|
||||
|--------|------|------|
|
||||
| ⏸️ 时长滑动条(5-60分钟) | **延后到 V1.1** | 固定 25 分钟降低复杂度 |
|
||||
| ⏸️ 白噪音播放 | **延后到 V1.1** | 需要音频资源 + 测试成本高 |
|
||||
| ⏸️ PDF 报告导出 | **延后到 V1.2** | 用户需求未验证,开发成本高 |
|
||||
| ⏸️ Lottie 动画 | **简化为静态** | 节省 3 天开发时间 |
|
||||
| ⏸️ 主题皮肤系统 | **改用文字徽章** | 设计成本太高 |
|
||||
| ⏸️ 每周趋势图表 | **仅显示今日** | 图表库集成复杂 |
|
||||
| ❌ TopOn 广告聚合 | **删除** | AdMob 已足够,过度优化 |
|
||||
| ❌ Body Doubling Lite | **删除** | 概念模糊,非核心价值 |
|
||||
### 2. 核心功能
|
||||
|
||||
#### MVP 保留的核心功能(3 个页面):
|
||||
|
||||
✅ **Home** - 一键开始专注(25 分钟固定)
|
||||
✅ **Focus** - "I got distracted" 按钮 + 4 种分心分类
|
||||
✅ **Complete** - 今日统计 + 鼓励文案 + "Start Another"
|
||||
|
||||
**附加简化页面:**
|
||||
- History(仅显示当天记录列表)
|
||||
- Settings(默认时长 3 选项 + 隐私政策链接)
|
||||
| 功能 | 说明 |
|
||||
|------|------|
|
||||
| **无惩罚机制** | 分心不中断计时,不断连成就,不重置进度 |
|
||||
| **分心记录** | "I got distracted"按钮 + 4种分心分类(社交媒体、被打断、感到压力、走神) |
|
||||
| **温柔鼓励** | 随机显示15条鼓励文案,如"Showing up is half the battle" |
|
||||
| **本地存储** | 使用Hive进行数据存储,所有数据仅存于设备 |
|
||||
| **多语言支持** | 支持14种语言(英语、中文、日语、韩语、西班牙语、德语、法语、葡萄牙语、俄语、印地语、印度尼西亚语、意大利语、阿拉伯语) |
|
||||
| **通知功能** | 后台计时通知,提醒用户正在计时中 |
|
||||
| **积分系统** | 完成专注获得积分,提升等级 |
|
||||
| **提前停止确认** | 点击Stop时友好提示,防止误操作 |
|
||||
| **空状态提示** | History页无数据时引导用户 |
|
||||
|
||||
---
|
||||
|
||||
### 2. 新增必要功能
|
||||
### 3. 技术栈实现
|
||||
|
||||
#### 原方案缺失的功能:
|
||||
|
||||
| 新增功能 | 优先级 | 开发时间 | 用途 |
|
||||
|---------|--------|---------|------|
|
||||
| **Onboarding 引导页** | P0 | 1 天 | 解释"无惩罚"理念,降低用户困惑 |
|
||||
| **空状态提示** | P0 | 0.5 天 | History 页无数据时引导用户 |
|
||||
| **后台计时通知** | P1 | 0.5 天 | 切到后台时提醒"正在计时中" |
|
||||
| **提前停止确认** | P1 | 0.5 天 | 点击 Stop 时友好提示 |
|
||||
|
||||
---
|
||||
|
||||
### 3. 技术栈优化
|
||||
|
||||
#### 依赖包精简(减少 4 个依赖):
|
||||
|
||||
**MVP 必须集成:**
|
||||
**已集成依赖包:**
|
||||
```yaml
|
||||
dependencies:
|
||||
flutter: ^3.10.0-290.4.beta
|
||||
flutter_localizations: ^0.1.0
|
||||
cupertino_icons: ^1.0.8
|
||||
hive: ^2.2.3 # 本地存储
|
||||
hive_flutter: ^1.1.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
|
||||
# workmanager: ^0.5.2 # 后台任务(MVP 不需要)
|
||||
# lottie: ^3.0.0 # 动画(用静态替代)
|
||||
# just_audio: ^0.9.36 # 音频(延后)
|
||||
# pdf: ^3.10.0 # PDF导出(延后)
|
||||
# google_mobile_ads: ^4.0.0 # 广告(V1.0.1 再加)
|
||||
dev_dependencies:
|
||||
flutter_test: ^0.0.0
|
||||
flutter_lints: ^6.0.0
|
||||
hive_generator: ^2.0.0 # Hive代码生成
|
||||
build_runner: ^2.4.0 # 构建工具
|
||||
```
|
||||
|
||||
**节省开发时间**: 约 2-3 天
|
||||
---
|
||||
|
||||
## 📱 应用特点
|
||||
|
||||
### 1. 无惩罚机制
|
||||
- 分心不中断计时
|
||||
- 不断连成就
|
||||
- 不重置进度
|
||||
- 温柔鼓励文案
|
||||
|
||||
### 2. 本地优先
|
||||
- 所有数据仅存于设备
|
||||
- 不联网、不上传
|
||||
- 保护用户隐私
|
||||
|
||||
### 3. 情绪友好
|
||||
- 柔和的颜色搭配
|
||||
- 清晰的字体设计
|
||||
- 简单的交互流程
|
||||
- 温暖的鼓励文案
|
||||
|
||||
### 4. 多语言支持
|
||||
- 支持14种语言
|
||||
- 本地化资源完整
|
||||
- 支持动态切换语言
|
||||
|
||||
---
|
||||
|
||||
### 4. 开发路线图调整
|
||||
## 🚀 上线准备
|
||||
|
||||
#### 原方案(4 周,过于激进):
|
||||
### 1. 应用商店准备
|
||||
|
||||
| 周数 | 原计划 | 风险 |
|
||||
|-----|--------|------|
|
||||
| 第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 天审核)
|
||||
**iOS App Store:**
|
||||
- [ ] 注册 Apple Developer 账号($99,需 1-2 天审核)
|
||||
- [ ] 准备 App 图标 1024×1024
|
||||
- [ ] 准备 6.5" iPhone 截图(至少 3 张)
|
||||
- [ ] 托管隐私政策(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 Console 账号
|
||||
**Google Play Store:**
|
||||
- [ ] 注册 Google Play Console 账号($25,立即生效)
|
||||
- [ ] 准备 App 图标 512×512
|
||||
- [ ] 准备截图(至少 2 张)
|
||||
- [ ] 填写应用描述(见 [app-store-metadata.md](app-store-metadata.md:110-191))
|
||||
- [ ] 填写应用描述(见 [app-store-metadata.md](app-store-metadata.md))
|
||||
|
||||
**合规文档(⚠️ 必须):**
|
||||
- [ ] 填写 [privacy-policy.md](privacy-policy.md:4) 开发者信息
|
||||
### 2. 合规文档
|
||||
|
||||
- [ ] 托管 [privacy-policy.md](privacy-policy.md) 到可访问的 URL
|
||||
- [ ] 托管 [terms-of-service.md](terms-of-service.md) 到可访问的 URL
|
||||
- [ ] 创建支持邮箱: focusbuddy.support@gmail.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%,说明核心价值不成立,需要重新思考 |
|
||||
- [ ] 创建支持邮箱: focusbuddy.app@outlook.com
|
||||
|
||||
---
|
||||
|
||||
@@ -179,131 +136,34 @@ dependencies:
|
||||
| **下载量** | > 500 | App Store Connect / Play Console |
|
||||
| **Day1 留存** | > 40% | 手动记录(对比首日下载 vs 次日活跃) |
|
||||
| **Day7 留存** | > 20% | 同上 |
|
||||
| **人均完成专注数** | > 3 次/周 | 后端分析(如果加了 Firebase) |
|
||||
| **人均完成专注数** | > 3 次/周 | 本地数据统计 |
|
||||
| **Crash 率** | < 2% | Firebase Crashlytics(免费版) |
|
||||
| **评分** | > 4.0 | App Store / Play Store |
|
||||
|
||||
**如果指标不达标** → 说明产品体验有问题,需要:
|
||||
1. 收集用户反馈(邮件 + Reddit 评论)
|
||||
2. 分析流失环节(哪一步用户离开了?)
|
||||
3. 快速迭代核心功能
|
||||
---
|
||||
|
||||
## 💡 产品亮点
|
||||
|
||||
### 1. 无惩罚专注
|
||||
- 传统番茄钟工具强调"完成",失败即惩罚
|
||||
- FocusBuddy 允许分心,鼓励温柔回归
|
||||
- 降低用户焦虑,提高持续使用意愿
|
||||
|
||||
### 2. 本地优先
|
||||
- 所有数据仅存于设备,保护用户隐私
|
||||
- 无需账号,无需联网,随时可用
|
||||
- 适合注重隐私的用户
|
||||
|
||||
### 3. 情绪友好
|
||||
- 柔和的颜色搭配,减少视觉刺激
|
||||
- 温暖的鼓励文案,增强用户信心
|
||||
- 简单的交互流程,降低使用门槛
|
||||
|
||||
### 4. 多语言支持
|
||||
- 支持14种语言,覆盖全球主要市场
|
||||
- 本地化资源完整,提供良好的用户体验
|
||||
|
||||
---
|
||||
|
||||
## 🚀 接下来的行动步骤
|
||||
|
||||
### 立即执行(今天):
|
||||
|
||||
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日
|
||||
**文档状态:** ✅ 已完成 MVP 版本开发
|
||||
**最后更新:** 2025年11月27日
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import java.util.Properties
|
||||
import java.io.FileInputStream
|
||||
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("kotlin-android")
|
||||
@@ -5,6 +8,13 @@ plugins {
|
||||
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 {
|
||||
namespace = "com.focusbuddy.focus_buddy"
|
||||
compileSdk = flutter.compileSdkVersion
|
||||
@@ -31,11 +41,18 @@ android {
|
||||
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 {
|
||||
release {
|
||||
// TODO: Add your own signing config for the release build.
|
||||
// Signing with the debug keys for now, so `flutter run --release` works.
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
signingConfig = signingConfigs.getByName("release")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
15
android/app/src/main/res/drawable/ic_notification.xml
Normal file
15
android/app/src/main/res/drawable/ic_notification.xml
Normal 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>
|
||||
BIN
android/release/app-release.aab
Normal file
BIN
android/release/app-release.aab
Normal file
Binary file not shown.
@@ -2,7 +2,7 @@
|
||||
|
||||
**Product:** FocusBuddy
|
||||
**Version:** 1.0 (MVP)
|
||||
**Last Updated:** November 22, 2025
|
||||
**Last Updated:** 2025年11月27日
|
||||
|
||||
---
|
||||
|
||||
|
||||
70
lib/components/control_buttons.dart
Normal file
70
lib/components/control_buttons.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
51
lib/components/distraction_button.dart
Normal file
51
lib/components/distraction_button.dart
Normal 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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
27
lib/components/timer_display.dart
Normal file
27
lib/components/timer_display.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -121,5 +121,84 @@
|
||||
"hindi": "हिन्दी",
|
||||
"indonesian": "Bahasa Indonesia",
|
||||
"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": "س"
|
||||
}
|
||||
|
||||
@@ -121,5 +121,84 @@
|
||||
"hindi": "हिन्दी",
|
||||
"indonesian": "Bahasa Indonesia",
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -231,5 +231,405 @@
|
||||
"hindi": "हिन्दी (Hindi)",
|
||||
"indonesian": "Bahasa Indonesia (Indonesian)",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,5 +121,84 @@
|
||||
"hindi": "हिन्दी",
|
||||
"indonesian": "Bahasa Indonesia",
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -121,5 +121,84 @@
|
||||
"hindi": "हिन्दी",
|
||||
"indonesian": "Bahasa Indonesia",
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -121,5 +121,84 @@
|
||||
"hindi": "हिन्दी",
|
||||
"indonesian": "Bahasa Indonesia",
|
||||
"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": "श"
|
||||
}
|
||||
|
||||
@@ -121,5 +121,84 @@
|
||||
"hindi": "हिन्दी",
|
||||
"indonesian": "Bahasa Indonesia",
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -121,5 +121,84 @@
|
||||
"hindi": "हिन्दी",
|
||||
"indonesian": "Bahasa Indonesia",
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -121,5 +121,84 @@
|
||||
"hindi": "हिन्दी",
|
||||
"indonesian": "Bahasa Indonesia",
|
||||
"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": "土"
|
||||
}
|
||||
|
||||
@@ -121,5 +121,84 @@
|
||||
"hindi": "हिन्दी",
|
||||
"indonesian": "Bahasa Indonesia",
|
||||
"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": "토"
|
||||
}
|
||||
|
||||
@@ -656,6 +656,456 @@ abstract class AppLocalizations {
|
||||
/// In en, this message translates to:
|
||||
/// **'العربية (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
|
||||
|
||||
@@ -333,4 +333,246 @@ class AppLocalizationsAr extends AppLocalizations {
|
||||
|
||||
@override
|
||||
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 => 'س';
|
||||
}
|
||||
|
||||
@@ -336,4 +336,250 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
|
||||
@override
|
||||
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';
|
||||
}
|
||||
|
||||
@@ -334,4 +334,251 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
|
||||
@override
|
||||
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';
|
||||
}
|
||||
|
||||
@@ -337,4 +337,256 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
|
||||
@override
|
||||
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';
|
||||
}
|
||||
|
||||
@@ -337,4 +337,261 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
|
||||
@override
|
||||
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';
|
||||
}
|
||||
|
||||
@@ -336,4 +336,249 @@ class AppLocalizationsHi extends AppLocalizations {
|
||||
|
||||
@override
|
||||
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 => 'श';
|
||||
}
|
||||
|
||||
@@ -336,4 +336,251 @@ class AppLocalizationsId extends AppLocalizations {
|
||||
|
||||
@override
|
||||
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';
|
||||
}
|
||||
|
||||
@@ -338,4 +338,260 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
|
||||
@override
|
||||
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';
|
||||
}
|
||||
|
||||
@@ -329,4 +329,243 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
|
||||
@override
|
||||
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 => '土';
|
||||
}
|
||||
|
||||
@@ -330,4 +330,243 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
|
||||
@override
|
||||
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 => '토';
|
||||
}
|
||||
|
||||
@@ -335,4 +335,252 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
|
||||
@override
|
||||
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';
|
||||
}
|
||||
|
||||
@@ -341,4 +341,249 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
|
||||
@override
|
||||
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 => 'С';
|
||||
}
|
||||
|
||||
@@ -326,4 +326,243 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
|
||||
@override
|
||||
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 => '六';
|
||||
}
|
||||
|
||||
@@ -121,5 +121,84 @@
|
||||
"hindi": "हिन्दी",
|
||||
"indonesian": "Bahasa Indonesia",
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -121,5 +121,84 @@
|
||||
"hindi": "हिन्दी",
|
||||
"indonesian": "Bahasa Indonesia",
|
||||
"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": "С"
|
||||
}
|
||||
|
||||
@@ -121,5 +121,84 @@
|
||||
"hindi": "हिन्दी",
|
||||
"indonesian": "Bahasa Indonesia",
|
||||
"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": "六"
|
||||
}
|
||||
|
||||
@@ -2,9 +2,8 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'l10n/app_localizations.dart';
|
||||
import 'theme/app_theme.dart';
|
||||
import 'services/storage_service.dart';
|
||||
import 'services/di.dart';
|
||||
import 'services/encouragement_service.dart';
|
||||
import 'services/notification_service.dart';
|
||||
import 'screens/home_screen.dart';
|
||||
import 'screens/onboarding_screen.dart';
|
||||
import 'screens/settings_screen.dart';
|
||||
@@ -12,19 +11,10 @@ import 'screens/settings_screen.dart';
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
// Initialize services
|
||||
await StorageService.init();
|
||||
// Initialize dependency injection
|
||||
await initializeDI();
|
||||
|
||||
final encouragementService = 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));
|
||||
runApp(MyApp(encouragementService: getIt<EncouragementService>()));
|
||||
}
|
||||
|
||||
class MyApp extends StatefulWidget {
|
||||
|
||||
190
lib/models/achievement_config.dart
Normal file
190
lib/models/achievement_config.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
144
lib/models/user_progress.dart
Normal file
144
lib/models/user_progress.dart
Normal 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];
|
||||
}
|
||||
}
|
||||
65
lib/models/user_progress.g.dart
Normal file
65
lib/models/user_progress.g.dart
Normal 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;
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import '../theme/app_colors.dart';
|
||||
import '../theme/app_text_styles.dart';
|
||||
import '../services/storage_service.dart';
|
||||
import '../services/encouragement_service.dart';
|
||||
import '../models/achievement_config.dart';
|
||||
import 'home_screen.dart';
|
||||
import 'history_screen.dart';
|
||||
|
||||
@@ -11,12 +12,22 @@ import 'history_screen.dart';
|
||||
class CompleteScreen extends StatelessWidget {
|
||||
final int focusedMinutes;
|
||||
final int distractionCount;
|
||||
final int pointsEarned;
|
||||
final int basePoints;
|
||||
final int honestyBonus;
|
||||
final int totalPoints;
|
||||
final List<String> newAchievements;
|
||||
final EncouragementService encouragementService;
|
||||
|
||||
const CompleteScreen({
|
||||
super.key,
|
||||
required this.focusedMinutes,
|
||||
required this.distractionCount,
|
||||
required this.pointsEarned,
|
||||
required this.basePoints,
|
||||
required this.honestyBonus,
|
||||
required this.totalPoints,
|
||||
this.newAchievements = const [],
|
||||
required this.encouragementService,
|
||||
});
|
||||
|
||||
@@ -33,99 +44,423 @@ class CompleteScreen extends StatelessWidget {
|
||||
body: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// Success Icon
|
||||
const Text(
|
||||
'✨',
|
||||
style: TextStyle(fontSize: 64),
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const SizedBox(height: 40),
|
||||
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// You focused for X minutes
|
||||
Text(
|
||||
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,
|
||||
// You focused for X minutes with success icon - left-right layout
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
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,
|
||||
// Star icon on the left
|
||||
const Text('✨', style: TextStyle(fontSize: 64)),
|
||||
const SizedBox(width: 20),
|
||||
// Text content on the right
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(l10n.youFocusedFor, style: AppTextStyles.headline),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
l10n.minutesValue(
|
||||
focusedMinutes,
|
||||
l10n.minutes(focusedMinutes),
|
||||
),
|
||||
style: AppTextStyles.largeNumber,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 40),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// Start Another Button
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
// Points Earned Section
|
||||
_buildPointsCard(context, l10n),
|
||||
|
||||
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: () {
|
||||
Navigator.pushAndRemoveUntil(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => HomeScreen(
|
||||
encouragementService: encouragementService,
|
||||
),
|
||||
builder: (context) => const HistoryScreen(),
|
||||
),
|
||||
(route) => false,
|
||||
(route) => route.isFirst,
|
||||
);
|
||||
},
|
||||
child: Text(l10n.startAnother),
|
||||
child: Text(l10n.viewHistory),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 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),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 40),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,9 +5,15 @@ import '../theme/app_colors.dart';
|
||||
import '../theme/app_text_styles.dart';
|
||||
import '../models/distraction_type.dart';
|
||||
import '../models/focus_session.dart';
|
||||
import '../services/di.dart';
|
||||
import '../services/storage_service.dart';
|
||||
import '../services/encouragement_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';
|
||||
|
||||
/// Focus Screen - Timer and distraction tracking
|
||||
@@ -32,7 +38,10 @@ class _FocusScreenState extends State<FocusScreen> with WidgetsBindingObserver {
|
||||
final List<String> _distractions = [];
|
||||
bool _isPaused = 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
|
||||
void initState() {
|
||||
@@ -80,7 +89,8 @@ class _FocusScreenState extends State<FocusScreen> with WidgetsBindingObserver {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final minutes = _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(
|
||||
remainingMinutes: minutes,
|
||||
remainingSeconds: seconds,
|
||||
@@ -102,7 +112,8 @@ class _FocusScreenState extends State<FocusScreen> with WidgetsBindingObserver {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final minutes = _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(
|
||||
remainingMinutes: minutes,
|
||||
remainingSeconds: seconds,
|
||||
@@ -125,7 +136,8 @@ class _FocusScreenState extends State<FocusScreen> with WidgetsBindingObserver {
|
||||
// Cancel ongoing notification and show completion notification
|
||||
await _notificationService.cancelOngoingFocusNotification();
|
||||
|
||||
_saveFocusSession(completed: true);
|
||||
// Calculate points and update user progress
|
||||
final pointsData = await _saveFocusSession(completed: true);
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
@@ -157,6 +169,11 @@ class _FocusScreenState extends State<FocusScreen> with WidgetsBindingObserver {
|
||||
builder: (context) => CompleteScreen(
|
||||
focusedMinutes: widget.durationMinutes,
|
||||
distractionCount: _distractions.length,
|
||||
pointsEarned: pointsData['pointsEarned']!,
|
||||
basePoints: pointsData['basePoints']!,
|
||||
honestyBonus: pointsData['honestyBonus']!,
|
||||
totalPoints: pointsData['totalPoints']!,
|
||||
newAchievements: pointsData['newAchievements'] as List<String>,
|
||||
encouragementService: widget.encouragementService,
|
||||
),
|
||||
),
|
||||
@@ -178,8 +195,11 @@ class _FocusScreenState extends State<FocusScreen> with WidgetsBindingObserver {
|
||||
|
||||
void _stopEarly() {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final actualMinutes = ((widget.durationMinutes * 60 - _remainingSeconds) / 60).floor();
|
||||
final minuteText = actualMinutes == 1 ? l10n.minutes(1) : l10n.minutes(actualMinutes);
|
||||
final actualMinutes =
|
||||
((widget.durationMinutes * 60 - _remainingSeconds) / 60).floor();
|
||||
final minuteText = actualMinutes == 1
|
||||
? l10n.minutes(1)
|
||||
: l10n.minutes(actualMinutes);
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
@@ -195,21 +215,37 @@ class _FocusScreenState extends State<FocusScreen> with WidgetsBindingObserver {
|
||||
child: Text(l10n.keepGoing),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context); // Close dialog
|
||||
onPressed: () async {
|
||||
// Close dialog immediately
|
||||
Navigator.pop(context);
|
||||
_timer.cancel();
|
||||
_saveFocusSession(completed: false);
|
||||
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => CompleteScreen(
|
||||
focusedMinutes: actualMinutes,
|
||||
distractionCount: _distractions.length,
|
||||
encouragementService: widget.encouragementService,
|
||||
),
|
||||
),
|
||||
);
|
||||
// Calculate points and update user progress
|
||||
final pointsData = await _saveFocusSession(completed: false);
|
||||
|
||||
// Create a new context for navigation
|
||||
if (mounted) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
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),
|
||||
),
|
||||
@@ -218,22 +254,65 @@ class _FocusScreenState extends State<FocusScreen> with WidgetsBindingObserver {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _saveFocusSession({required bool completed}) async {
|
||||
final actualMinutes = completed
|
||||
? widget.durationMinutes
|
||||
: ((widget.durationMinutes * 60 - _remainingSeconds) / 60).floor();
|
||||
Future<Map<String, dynamic>> _saveFocusSession({
|
||||
required bool completed,
|
||||
}) async {
|
||||
try {
|
||||
final actualMinutes = completed
|
||||
? widget.durationMinutes
|
||||
: ((widget.durationMinutes * 60 - _remainingSeconds) / 60).floor();
|
||||
|
||||
final session = FocusSession(
|
||||
startTime: _startTime,
|
||||
durationMinutes: widget.durationMinutes,
|
||||
actualMinutes: actualMinutes,
|
||||
distractionCount: _distractions.length,
|
||||
completed: completed,
|
||||
distractionTypes: _distractions,
|
||||
);
|
||||
final session = FocusSession(
|
||||
startTime: _startTime,
|
||||
durationMinutes: widget.durationMinutes,
|
||||
actualMinutes: actualMinutes,
|
||||
distractionCount: _distractions.length,
|
||||
completed: completed,
|
||||
distractionTypes: _distractions,
|
||||
);
|
||||
|
||||
final storageService = StorageService();
|
||||
await storageService.saveFocusSession(session);
|
||||
// Save 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() {
|
||||
@@ -241,7 +320,10 @@ class _FocusScreenState extends State<FocusScreen> with WidgetsBindingObserver {
|
||||
|
||||
// Map distraction types to translations
|
||||
final distractionOptions = [
|
||||
(type: DistractionType.phoneNotification, label: l10n.distractionPhoneNotification),
|
||||
(
|
||||
type: DistractionType.phoneNotification,
|
||||
label: l10n.distractionPhoneNotification,
|
||||
),
|
||||
(type: DistractionType.socialMedia, label: l10n.distractionSocialMedia),
|
||||
(type: DistractionType.thoughts, label: l10n.distractionThoughts),
|
||||
(type: DistractionType.other, label: l10n.distractionOther),
|
||||
@@ -339,30 +421,26 @@ class _FocusScreenState extends State<FocusScreen> with WidgetsBindingObserver {
|
||||
}
|
||||
|
||||
void _recordDistraction(String? type) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
|
||||
setState(() {
|
||||
if (type != null) {
|
||||
_distractions.add(type);
|
||||
}
|
||||
});
|
||||
|
||||
// Show encouragement toast
|
||||
// Show distraction-specific encouragement toast
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(l10n.distractionEncouragement),
|
||||
content: Text(
|
||||
widget.encouragementService.getRandomMessage(
|
||||
EncouragementType.distraction,
|
||||
),
|
||||
),
|
||||
duration: const Duration(seconds: 2),
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
@@ -371,106 +449,30 @@ class _FocusScreenState extends State<FocusScreen> with WidgetsBindingObserver {
|
||||
backgroundColor: AppColors.background,
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: MediaQuery.of(context).size.height * 0.2,
|
||||
),
|
||||
// Timer Display Component
|
||||
TimerDisplay(remainingSeconds: _remainingSeconds),
|
||||
|
||||
// Timer Display
|
||||
Text(
|
||||
_formatTime(_remainingSeconds),
|
||||
style: AppTextStyles.timerDisplay,
|
||||
),
|
||||
const SizedBox(height: 80),
|
||||
|
||||
const SizedBox(height: 80),
|
||||
|
||||
// "I got distracted" Button
|
||||
SizedBox(
|
||||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// "I got distracted" Button Component
|
||||
DistractionButton(
|
||||
onPressed: _showDistractionSheet,
|
||||
buttonText: l10n.iGotDistracted,
|
||||
),
|
||||
|
||||
// Stop Button (text button at bottom)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 24.0),
|
||||
child: TextButton(
|
||||
onPressed: _stopEarly,
|
||||
child: Text(
|
||||
l10n.stopSession,
|
||||
style: const TextStyle(
|
||||
color: AppColors.textSecondary,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Control Buttons Component
|
||||
ControlButtons(
|
||||
isPaused: _isPaused,
|
||||
onTogglePause: _togglePause,
|
||||
onStopEarly: _stopEarly,
|
||||
pauseText: l10n.pause,
|
||||
resumeText: l10n.resume,
|
||||
stopText: l10n.stopSession,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
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 '../services/storage_service.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import '../l10n/app_localizations.dart';
|
||||
import 'session_detail_screen.dart';
|
||||
|
||||
/// History Screen - Shows past focus sessions
|
||||
class HistoryScreen extends StatefulWidget {
|
||||
@@ -81,10 +82,7 @@ class _HistoryScreenState extends State<HistoryScreen> {
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Text(
|
||||
'📊',
|
||||
style: TextStyle(fontSize: 64),
|
||||
),
|
||||
const Text('📊', style: TextStyle(fontSize: 64)),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
l10n.noFocusSessionsYet,
|
||||
@@ -100,7 +98,7 @@ class _HistoryScreenState extends State<HistoryScreen> {
|
||||
const SizedBox(height: 32),
|
||||
ElevatedButton(
|
||||
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(
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
@@ -155,13 +158,20 @@ class _HistoryScreenState extends State<HistoryScreen> {
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildStat('Total', l10n.minutesValue(totalMins, l10n.minutes(totalMins)), '⏱️'),
|
||||
child: _buildStat(
|
||||
l10n.total,
|
||||
l10n.minutesValue(totalMins, l10n.minutes(totalMins)),
|
||||
'⏱️',
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: _buildStat(
|
||||
'Distractions',
|
||||
l10n.distractionsCount(distractions, l10n.times(distractions)),
|
||||
l10n.distractions(distractions),
|
||||
l10n.distractionsCount(
|
||||
distractions,
|
||||
l10n.times(distractions),
|
||||
),
|
||||
'🤚',
|
||||
),
|
||||
),
|
||||
@@ -176,10 +186,7 @@ class _HistoryScreenState extends State<HistoryScreen> {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
emoji,
|
||||
style: const TextStyle(fontSize: 24),
|
||||
),
|
||||
Text(emoji, style: const TextStyle(fontSize: 24)),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
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 dateLabel = isToday
|
||||
? l10n.today
|
||||
@@ -257,83 +268,113 @@ class _HistoryScreenState extends State<HistoryScreen> {
|
||||
final statusEmoji = session.completed ? '✅' : '⏸️';
|
||||
final statusText = session.completed ? l10n.completed : l10n.stoppedEarly;
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: AppColors.divider,
|
||||
width: 1,
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => SessionDetailScreen(session: session),
|
||||
),
|
||||
);
|
||||
},
|
||||
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(
|
||||
children: [
|
||||
// Time
|
||||
Text(
|
||||
timeStr,
|
||||
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(
|
||||
child: Row(
|
||||
children: [
|
||||
// Time
|
||||
Text(
|
||||
timeStr,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Nunito',
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: session.completed
|
||||
? AppColors.success
|
||||
: AppColors.textSecondary,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,9 +3,12 @@ import '../l10n/app_localizations.dart';
|
||||
import '../theme/app_colors.dart';
|
||||
import '../theme/app_text_styles.dart';
|
||||
import '../services/encouragement_service.dart';
|
||||
import '../services/storage_service.dart';
|
||||
import '../services/di.dart';
|
||||
import 'focus_screen.dart';
|
||||
import 'history_screen.dart';
|
||||
import 'settings_screen.dart';
|
||||
import 'profile_screen.dart';
|
||||
|
||||
/// Home Screen - Loads default duration from settings
|
||||
class HomeScreen extends StatefulWidget {
|
||||
@@ -22,6 +25,7 @@ class HomeScreen extends StatefulWidget {
|
||||
|
||||
class _HomeScreenState extends State<HomeScreen> {
|
||||
int _defaultDuration = 25;
|
||||
final StorageService _storageService = getIt<StorageService>();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -30,15 +34,23 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
}
|
||||
|
||||
Future<void> _loadDefaultDuration() async {
|
||||
final duration = await SettingsScreen.getDefaultDuration();
|
||||
setState(() {
|
||||
_defaultDuration = duration;
|
||||
});
|
||||
try {
|
||||
final duration = await SettingsScreen.getDefaultDuration();
|
||||
setState(() {
|
||||
_defaultDuration = duration;
|
||||
});
|
||||
} catch (e) {
|
||||
// Use default duration if loading fails
|
||||
setState(() {
|
||||
_defaultDuration = 25;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final progress = _storageService.getUserProgress();
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.background,
|
||||
@@ -46,8 +58,12 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// Points Card at the top
|
||||
_buildPointsCard(context, progress),
|
||||
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// App Title
|
||||
Text(
|
||||
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) {
|
||||
_loadDefaultDuration();
|
||||
setState(() {}); // Refresh to show updated points
|
||||
}
|
||||
},
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
830
lib/screens/profile_screen.dart
Normal file
830
lib/screens/profile_screen.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
602
lib/screens/session_detail_screen.dart
Normal file
602
lib/screens/session_detail_screen.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,20 +11,34 @@ class SettingsScreen extends StatefulWidget {
|
||||
|
||||
/// Get the saved default duration (for use in other screens)
|
||||
static Future<int> getDefaultDuration() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getInt(_durationKey) ?? 25;
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getInt(_durationKey) ?? 25;
|
||||
} catch (e) {
|
||||
// Return default duration if loading fails
|
||||
return 25;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the saved locale
|
||||
static Future<String?> getSavedLocale() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getString(_localeKey);
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getString(_localeKey);
|
||||
} catch (e) {
|
||||
// Return null if loading fails
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Save the locale
|
||||
static Future<void> saveLocale(String localeCode) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(_localeKey, localeCode);
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(_localeKey, localeCode);
|
||||
} catch (e) {
|
||||
// Ignore save errors
|
||||
}
|
||||
}
|
||||
|
||||
static const String _durationKey = 'default_duration';
|
||||
@@ -48,36 +62,58 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
}
|
||||
|
||||
Future<void> _loadSavedDuration() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
setState(() {
|
||||
_selectedDuration = prefs.getInt(SettingsScreen._durationKey) ?? 25;
|
||||
});
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
setState(() {
|
||||
_selectedDuration = prefs.getInt(SettingsScreen._durationKey) ?? 25;
|
||||
});
|
||||
} catch (e) {
|
||||
// Use default duration if loading fails
|
||||
setState(() {
|
||||
_selectedDuration = 25;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadSavedLocale() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
setState(() {
|
||||
_selectedLocale = prefs.getString(SettingsScreen._localeKey) ?? 'en';
|
||||
});
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
setState(() {
|
||||
_selectedLocale = prefs.getString(SettingsScreen._localeKey) ?? 'en';
|
||||
});
|
||||
} catch (e) {
|
||||
// Use default locale if loading fails
|
||||
setState(() {
|
||||
_selectedLocale = 'en';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _saveDuration(int duration) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setInt(SettingsScreen._durationKey, duration);
|
||||
setState(() {
|
||||
_selectedDuration = duration;
|
||||
});
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setInt(SettingsScreen._durationKey, duration);
|
||||
setState(() {
|
||||
_selectedDuration = duration;
|
||||
});
|
||||
} catch (e) {
|
||||
// Ignore save errors, state will be reset on next load
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _saveLocale(String localeCode) async {
|
||||
await SettingsScreen.saveLocale(localeCode);
|
||||
setState(() {
|
||||
_selectedLocale = localeCode;
|
||||
});
|
||||
try {
|
||||
await SettingsScreen.saveLocale(localeCode);
|
||||
setState(() {
|
||||
_selectedLocale = localeCode;
|
||||
});
|
||||
|
||||
// Update locale immediately without restart
|
||||
if (!mounted) return;
|
||||
MyApp.updateLocale(context, localeCode);
|
||||
// Update locale immediately without restart
|
||||
if (!mounted) return;
|
||||
MyApp.updateLocale(context, localeCode);
|
||||
} catch (e) {
|
||||
// Ignore save errors
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
111
lib/services/achievement_service.dart
Normal file
111
lib/services/achievement_service.dart
Normal 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
57
lib/services/di.dart
Normal 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();
|
||||
}
|
||||
@@ -2,38 +2,154 @@ import 'dart:convert';
|
||||
import 'dart:math';
|
||||
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 {
|
||||
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();
|
||||
|
||||
/// Load encouragement messages from assets
|
||||
Future<void> loadMessages() async {
|
||||
try {
|
||||
final String jsonString =
|
||||
final String jsonString =
|
||||
await rootBundle.loadString('assets/encouragements.json');
|
||||
final List<dynamic> jsonList = json.decode(jsonString);
|
||||
_messages = jsonList.cast<String>();
|
||||
final dynamic jsonData = json.decode(jsonString);
|
||||
|
||||
// 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) {
|
||||
// Fallback messages if file can't be loaded
|
||||
_messages = [
|
||||
"Showing up is half the battle.",
|
||||
"Every minute counts.",
|
||||
"You're learning, not failing.",
|
||||
"Gentleness is strength.",
|
||||
"Progress over perfection.",
|
||||
];
|
||||
// Fallback to default messages if file can't be loaded
|
||||
_initializeDefaultMessages();
|
||||
}
|
||||
}
|
||||
|
||||
/// Load categorized messages from JSON map
|
||||
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
|
||||
String getRandomMessage() {
|
||||
if (_messages.isEmpty) {
|
||||
/// Get a random encouragement message for a specific type
|
||||
String getRandomMessage([EncouragementType type = EncouragementType.general]) {
|
||||
final messages = _messages[type] ?? [];
|
||||
if (messages.isEmpty) {
|
||||
return "You're doing great!";
|
||||
}
|
||||
return _messages[_random.nextInt(_messages.length)];
|
||||
return messages[_random.nextInt(messages.length)];
|
||||
}
|
||||
|
||||
/// Get all messages (for testing)
|
||||
List<String> getAllMessages() => List.from(_messages);
|
||||
/// Get all messages for a specific type (for testing)
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
@@ -9,10 +10,21 @@ class NotificationService {
|
||||
factory NotificationService() => _instance;
|
||||
NotificationService._internal();
|
||||
|
||||
final FlutterLocalNotificationsPlugin _notifications =
|
||||
final FlutterLocalNotificationsPlugin _notifications =
|
||||
FlutterLocalNotificationsPlugin();
|
||||
|
||||
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
|
||||
Future<void> initialize() async {
|
||||
@@ -28,7 +40,9 @@ class NotificationService {
|
||||
|
||||
try {
|
||||
// Android initialization settings
|
||||
const androidSettings = AndroidInitializationSettings('@mipmap/ic_launcher');
|
||||
const androidSettings = AndroidInitializationSettings(
|
||||
'@drawable/ic_notification',
|
||||
);
|
||||
|
||||
// iOS initialization settings
|
||||
const iosSettings = DarwinInitializationSettings(
|
||||
@@ -48,6 +62,13 @@ class NotificationService {
|
||||
);
|
||||
|
||||
_initialized = true;
|
||||
|
||||
// Start listening for permission changes
|
||||
await listenForPermissionChanges();
|
||||
|
||||
// Check initial permission status
|
||||
await hasPermission();
|
||||
|
||||
if (kDebugMode) {
|
||||
print('Notification service initialized successfully');
|
||||
}
|
||||
@@ -63,7 +84,6 @@ class NotificationService {
|
||||
if (kDebugMode) {
|
||||
print('Notification tapped: ${response.payload}');
|
||||
}
|
||||
// TODO: Navigate to appropriate screen if needed
|
||||
}
|
||||
|
||||
/// Request notification permissions (iOS and Android 13+)
|
||||
@@ -71,39 +91,43 @@ class NotificationService {
|
||||
if (kIsWeb) return false;
|
||||
|
||||
try {
|
||||
bool isGranted = false;
|
||||
|
||||
// Check if we're on Android or iOS
|
||||
if (Platform.isAndroid) {
|
||||
// Android 13+ requires runtime permission
|
||||
final status = await Permission.notification.request();
|
||||
isGranted = status.isGranted;
|
||||
|
||||
if (kDebugMode) {
|
||||
print('Android notification permission status: $status');
|
||||
}
|
||||
|
||||
return status.isGranted;
|
||||
} else if (Platform.isIOS) {
|
||||
// iOS permission request
|
||||
final result = await _notifications
|
||||
.resolvePlatformSpecificImplementation<
|
||||
IOSFlutterLocalNotificationsPlugin>()
|
||||
?.requestPermissions(
|
||||
alert: true,
|
||||
badge: true,
|
||||
sound: true,
|
||||
);
|
||||
|
||||
if (kDebugMode) {
|
||||
print('iOS notification permission result: $result');
|
||||
final iosImplementation = _notifications.resolvePlatformSpecificImplementation<IOSFlutterLocalNotificationsPlugin>();
|
||||
if (iosImplementation != null) {
|
||||
final result = await iosImplementation.requestPermissions(alert: true, badge: true, sound: true);
|
||||
isGranted = result ?? false;
|
||||
|
||||
if (kDebugMode) {
|
||||
print('iOS notification permission result: $result');
|
||||
}
|
||||
} else {
|
||||
isGranted = true; // Assume granted if we can't request
|
||||
}
|
||||
|
||||
return result ?? false;
|
||||
} else {
|
||||
isGranted = true; // Assume granted for other platforms
|
||||
}
|
||||
|
||||
return true; // Other platforms
|
||||
|
||||
// Update the permission status stream
|
||||
_permissionStatusController.add(isGranted);
|
||||
|
||||
return isGranted;
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print('Failed to request permissions: $e');
|
||||
}
|
||||
_permissionStatusController.add(false);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -113,21 +137,39 @@ class NotificationService {
|
||||
if (kIsWeb) return false;
|
||||
|
||||
try {
|
||||
bool isGranted = false;
|
||||
|
||||
if (Platform.isAndroid) {
|
||||
final status = await Permission.notification.status;
|
||||
return status.isGranted;
|
||||
isGranted = status.isGranted;
|
||||
} else if (Platform.isIOS) {
|
||||
// For iOS, we can't easily check without requesting, so we assume granted after request
|
||||
return true;
|
||||
// For iOS, we assume granted after initial request
|
||||
isGranted = true;
|
||||
} else {
|
||||
isGranted = true; // Assume granted for other platforms
|
||||
}
|
||||
return true;
|
||||
|
||||
// Update the permission status stream
|
||||
_permissionStatusController.add(isGranted);
|
||||
|
||||
return isGranted;
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print('Failed to check permission status: $e');
|
||||
}
|
||||
_permissionStatusController.add(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
|
||||
Future<void> showFocusCompletedNotification({
|
||||
@@ -147,6 +189,7 @@ class NotificationService {
|
||||
priority: Priority.high,
|
||||
enableVibration: true,
|
||||
playSound: true,
|
||||
icon: '@drawable/ic_notification',
|
||||
);
|
||||
|
||||
const iosDetails = DarwinNotificationDetails(
|
||||
@@ -162,7 +205,8 @@ class NotificationService {
|
||||
|
||||
// Use provided title/body or fall back to English
|
||||
final notificationTitle = title ?? '🎉 Focus session complete!';
|
||||
final notificationBody = body ??
|
||||
final notificationBody =
|
||||
body ??
|
||||
(distractionCount == 0
|
||||
? 'You focused for $minutes ${minutes == 1 ? 'minute' : 'minutes'} without distractions!'
|
||||
: 'You focused for $minutes ${minutes == 1 ? 'minute' : 'minutes'}. Great effort!');
|
||||
@@ -186,9 +230,7 @@ class NotificationService {
|
||||
}
|
||||
|
||||
/// Show reminder notification (optional feature for future)
|
||||
Future<void> showReminderNotification({
|
||||
required String message,
|
||||
}) async {
|
||||
Future<void> showReminderNotification({required String message}) async {
|
||||
if (kIsWeb || !_initialized) return;
|
||||
|
||||
try {
|
||||
@@ -198,6 +240,7 @@ class NotificationService {
|
||||
channelDescription: 'Gentle reminders to focus',
|
||||
importance: Importance.defaultImportance,
|
||||
priority: Priority.defaultPriority,
|
||||
icon: '@drawable/ic_notification',
|
||||
);
|
||||
|
||||
const iosDetails = DarwinNotificationDetails();
|
||||
@@ -259,7 +302,8 @@ class NotificationService {
|
||||
|
||||
try {
|
||||
// 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(
|
||||
'focus_timer',
|
||||
@@ -274,6 +318,7 @@ class NotificationService {
|
||||
playSound: false,
|
||||
// Show in status bar
|
||||
showProgress: false,
|
||||
icon: '@drawable/ic_notification',
|
||||
);
|
||||
|
||||
const iosDetails = DarwinNotificationDetails(
|
||||
|
||||
164
lib/services/points_service.dart
Normal file
164
lib/services/points_service.dart
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
99
lib/services/service_locator.dart
Normal file
99
lib/services/service_locator.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -1,62 +1,222 @@
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import '../models/focus_session.dart';
|
||||
import '../models/user_progress.dart';
|
||||
|
||||
/// Service to manage local storage using Hive
|
||||
class StorageService {
|
||||
static const String _focusSessionBox = 'focus_sessions';
|
||||
static const String _userProgressBox = 'user_progress';
|
||||
static const String _progressKey = 'user_progress_key';
|
||||
|
||||
/// Initialize Hive
|
||||
static Future<void> init() async {
|
||||
await Hive.initFlutter();
|
||||
// Cache for today's sessions to improve performance
|
||||
List<FocusSession>? _todaySessionsCache;
|
||||
DateTime? _cacheDate;
|
||||
|
||||
// Register adapters
|
||||
Hive.registerAdapter(FocusSessionAdapter());
|
||||
// Cache for user progress
|
||||
UserProgress? _userProgressCache;
|
||||
|
||||
// Open boxes
|
||||
await Hive.openBox<FocusSession>(_focusSessionBox);
|
||||
/// Initialize Hive storage service
|
||||
///
|
||||
/// 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
|
||||
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 {
|
||||
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() {
|
||||
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() {
|
||||
final now = DateTime.now();
|
||||
final today = DateTime(now.year, now.month, now.day);
|
||||
try {
|
||||
final now = DateTime.now();
|
||||
final today = DateTime(now.year, now.month, now.day);
|
||||
|
||||
return _sessionsBox.values.where((session) {
|
||||
final sessionDate = DateTime(
|
||||
session.startTime.year,
|
||||
session.startTime.month,
|
||||
session.startTime.day,
|
||||
);
|
||||
return sessionDate == today;
|
||||
}).toList();
|
||||
// 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();
|
||||
|
||||
// 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
|
||||
///
|
||||
/// Returns the sum of actual minutes focused today
|
||||
int getTodayTotalMinutes() {
|
||||
return getTodaySessions()
|
||||
.fold<int>(0, (sum, session) => sum + session.actualMinutes);
|
||||
}
|
||||
|
||||
/// Get total distractions for today
|
||||
///
|
||||
/// Returns the total number of distractions recorded today
|
||||
int getTodayDistractionCount() {
|
||||
return getTodaySessions()
|
||||
.fold<int>(0, (sum, session) => sum + session.distractionCount);
|
||||
}
|
||||
|
||||
/// Get total completed sessions for today
|
||||
///
|
||||
/// Returns the number of focus sessions completed today
|
||||
int getTodayCompletedCount() {
|
||||
return getTodaySessions()
|
||||
.where((session) => session.completed)
|
||||
@@ -64,22 +224,54 @@ class StorageService {
|
||||
}
|
||||
|
||||
/// Get total sessions count for today (including stopped early)
|
||||
///
|
||||
/// Returns the total number of focus sessions started today
|
||||
int getTodaySessionsCount() {
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
await Hive.close();
|
||||
try {
|
||||
await Hive.close();
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print('Failed to close Hive boxes: $e');
|
||||
}
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
169
lib/theme/app_constants.dart
Normal file
169
lib/theme/app_constants.dart
Normal 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;
|
||||
}
|
||||
@@ -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:** 根据实际开发进度调整里程碑
|
||||
@@ -1,6 +1,6 @@
|
||||
# Privacy Policy for FocusBuddy
|
||||
|
||||
**Last Updated**: November 22, 2025
|
||||
**Last Updated**: 2025年11月27日
|
||||
**Developer**: FocusBuddy Team
|
||||
**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.
|
||||
- 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:
|
||||
- Rewarded videos (e.g., “Watch ad to unlock a new theme”)
|
||||
- Occasional interstitial ads (shown after every few sessions, skippable)
|
||||
- AdMob may collect limited non-personal information (like device model, OS version, or approximate IP address) to serve relevant ads, in accordance with [Google’s 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”
|
||||
- The current version of FocusBuddy does not contain any advertising.
|
||||
- We do not use any ad networks or display any ads within the app.
|
||||
- No ad-related data is collected or processed.
|
||||
|
||||
## 3. No Analytics or Tracking SDKs
|
||||
|
||||
|
||||
@@ -1,230 +1,255 @@
|
||||
# ADHD 专注伴侣产品方案(个人开发者版)
|
||||
|
||||
> **产品名称**:FocusBuddy(暂定,备选:GentleFlow,MindAnchor,ComeBack Timer,SoftFocus)
|
||||
> **定位**:一款为神经多样性人群设计的、无惩罚、情感支持型专注工具
|
||||
> **目标**:帮助用户温柔地回到当下,而非追求“高效”
|
||||
> **适用平台**:iOS + Android(Flutter 跨平台)
|
||||
> **开发周期**:4–6 周 MVP
|
||||
> **作者**:个人开发者
|
||||
> **最后更新**:2025年11月22日
|
||||
|
||||
---
|
||||
|
||||
## 一、产品背景与市场机会
|
||||
|
||||
### 1.1 用户痛点
|
||||
- ADHD 及注意力困难人群常因“无法专注”产生自我批评;
|
||||
- 现有番茄钟工具强调“完成”,失败即惩罚(如 Forest 树枯死),加剧焦虑;
|
||||
- 用户需要的是“允许分心 + 温柔回归”的支持机制,而非效率压榨。
|
||||
|
||||
### 1.2 市场验证
|
||||
- 全球约 **4–5% 成年人**存在 ADHD 特征(CHADD 数据);
|
||||
- Reddit r/ADHD 拥有 **超 200 万订阅者**,TikTok #ADHDTips 话题播放量超 **10 亿**;
|
||||
- 竞品如 Tiimo(估值 $1 亿)、Focus Keeper(长期付费榜前列)证明付费意愿强;
|
||||
- **空白点**:缺乏轻量、离线、情绪友好的垂直工具。
|
||||
|
||||
### 1.3 为什么适合个人开发者?
|
||||
- 功能聚焦,无需后端;
|
||||
- 开发成本低(纯本地逻辑);
|
||||
- 广告变现路径清晰;
|
||||
- 社区自传播潜力大。
|
||||
|
||||
---
|
||||
|
||||
## 二、产品定位与原则
|
||||
|
||||
### 2.1 核心理念
|
||||
> “专注不是坚持不走神,而是每次走神后,都愿意轻轻回来。”
|
||||
|
||||
### 2.2 三大设计原则
|
||||
| 原则 | 说明 |
|
||||
|------|------|
|
||||
| **无惩罚机制** | 分心不中断计时,不断连成就,不重置进度 |
|
||||
| **本地优先** | 所有数据仅存于设备,不联网、不上传 |
|
||||
| **情绪友好** | 用鼓励文案、柔和动效、低刺激视觉降低焦虑 |
|
||||
|
||||
### 2.3 避免踩坑
|
||||
- ❌ 不使用 “ADHD”、“治疗”、“诊断” 等医疗词汇;
|
||||
- ✅ 定位为 “focus support tool for neurodivergent minds”;
|
||||
- ✅ 强调 “gentle”, “kind”, “no guilt”。
|
||||
|
||||
---
|
||||
|
||||
## 三、核心功能(MVP)
|
||||
|
||||
### 3.1 功能列表
|
||||
|
||||
| 模块 | 功能 | 说明 |
|
||||
|------|------|------|
|
||||
| **启动页** | 一键开始专注 | 默认 25 分钟,可滑动调整(5–60 分钟) |
|
||||
| **专注中** | “I got distracted” 按钮 | 点击记录分心类型,不中断计时 |
|
||||
| **分心分类** | 4 种常见场景 | • Scrolling social media<br>• Got interrupted<br>• Felt overwhelmed<br>• Just zoned out |
|
||||
| **温柔回归** | 鼓励反馈 | 显示文案:“It happens. Let’s gently come back.” + 轻柔音效 |
|
||||
| **专注报告** | 每日总结卡片 | 含总时长、分心趋势、随机鼓励语 |
|
||||
| **成就系统** | 连续完成奖励 | 解锁主题皮肤(如 “Calm Cloud”) |
|
||||
| **广告激励** | 可选看广告 | 解锁新主题或恢复断连(非强制) |
|
||||
|
||||
### 3.2 差异化亮点
|
||||
- **Body Doubling Lite**:未来可扩展静默陪伴视频(当前 MVP 暂不实现);
|
||||
- **ASMR 音效**:集成免费 CC 协议白噪音(雨声、键盘声);
|
||||
- **Export Report**:生成 PDF 周报(用户主动触发,用于与治疗师分享)。
|
||||
|
||||
---
|
||||
|
||||
## 四、UI/UX 设计
|
||||
|
||||
### 4.1 视觉风格
|
||||
- **色彩**:莫兰迪色系(主色 `#A7C4BC`,背景 `#F8F6F2`)
|
||||
- **字体**:Nunito(圆润、易读)
|
||||
- **图标**:手绘感、轻微不规则
|
||||
- **动效**:缓慢粒子飘动、按钮呼吸动画
|
||||
|
||||
### 4.2 核心页面(Figma 原型)
|
||||
|
||||
#### 页面 1:启动页(Home)
|
||||
```
|
||||
[居中大按钮] Start Focusing (25 min)
|
||||
[小字提示] Tap 'I got distracted' anytime — no guilt.
|
||||
```
|
||||
|
||||
#### 页面 2:专注中(During Focus)
|
||||
```
|
||||
24:37
|
||||
[按钮] I got distracted Pause
|
||||
(点击后弹出分心类型选项)
|
||||
```
|
||||
|
||||
#### 页面 3:专注报告(Summary)
|
||||
```
|
||||
✅ You focused for 24 minutes today.
|
||||
📊 Distractions: 2 times
|
||||
🌱 Achievement unlocked: "Calm Cloud"
|
||||
[按钮] Watch ad to unlock next theme
|
||||
```
|
||||
|
||||
### 4.3 鼓励文案库(随机展示)
|
||||
- “Showing up is half the battle.”
|
||||
- “Every minute counts.”
|
||||
- “You’re learning, not failing.”
|
||||
- “Gentleness is strength.”
|
||||
|
||||
---
|
||||
|
||||
## 五、技术实现
|
||||
|
||||
### 5.1 技术栈
|
||||
| 组件 | 方案 |
|
||||
|------|------|
|
||||
| 跨平台框架 | Flutter |
|
||||
| 本地存储 | Hive(加密支持) |
|
||||
| 定时与通知 | flutter_local_notifications + workmanager |
|
||||
| 动画 | Lottie / Rive |
|
||||
| 音频 | just_audio |
|
||||
| 广告 | Google AdMob + TopOn 聚合(可选) |
|
||||
|
||||
### 5.2 数据结构(Hive)
|
||||
```dart
|
||||
class FocusSession {
|
||||
DateTime startTime;
|
||||
int durationMinutes;
|
||||
List<Distraction> distractions;
|
||||
}
|
||||
|
||||
class Distraction {
|
||||
String type; // e.g., "social", "interrupted"
|
||||
DateTime time;
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 开发里程碑
|
||||
| 周数 | 目标 |
|
||||
|------|------|
|
||||
| 第1周 | UI + 基础计时器 |
|
||||
| 第2周 | 分心记录 + Hive 存储 |
|
||||
| 第3周 | 报告生成 + 成就系统 |
|
||||
| 第4周 | 广告接入 + 测试发布 |
|
||||
|
||||
### 5.4 多语言支持
|
||||
#### 高优先级
|
||||
日语 (Japanese) 🇯🇵
|
||||
原因: 日本对生产力工具和专注应用有极高需求
|
||||
特点: ADHD 和神经多样性支持在日本很受关注
|
||||
市场: 日本的 App Store 付费意愿很高
|
||||
韩语 (Korean) 🇰🇷
|
||||
原因: 韩国学生和上班族对学习/工作效率工具需求很大
|
||||
特点: "番茄工作法"和专注应用在韩国非常流行
|
||||
市场: K-pop 文化影响,年轻用户群体活跃
|
||||
西班牙语 (Spanish) 🇪🇸 🇲🇽
|
||||
原因: 全球第二大母语人口(4.5亿+)
|
||||
覆盖: 西班牙、墨西哥、阿根廷、哥伦比亚等20+国家
|
||||
市场: 拉丁美洲移动应用市场快速增长
|
||||
#### 中等优先级
|
||||
德语 (German) 🇩🇪
|
||||
德国、奥地利、瑞士
|
||||
注重隐私和离线功能(你的卖点!)
|
||||
付费意愿高
|
||||
法语 (French) 🇫🇷
|
||||
法国、加拿大(魁北克)、比利时、瑞士
|
||||
约3亿使用者
|
||||
葡萄牙语 (Portuguese) 🇧🇷
|
||||
巴西(2.2亿人口)
|
||||
快速增长的移动市场
|
||||
俄语 (Russian) 🇷🇺
|
||||
俄罗斯、独联体国家
|
||||
约2.6亿使用者
|
||||
#### 长期考虑
|
||||
意大利语 (Italian) 🇮🇹
|
||||
荷兰语 (Dutch) 🇳🇱
|
||||
土耳其语 (Turkish) 🇹🇷
|
||||
|
||||
---
|
||||
|
||||
## 六、合规与隐私
|
||||
|
||||
### 6.1 隐私政策要点
|
||||
- **无数据收集**:所有数据仅存于设备;
|
||||
- **无分析 SDK**:不使用 Firebase、GA 等;
|
||||
- **广告透明**:说明 AdMob 使用,提供个性化广告关闭指引;
|
||||
- **非医疗工具**:明确声明不用于诊断或治疗。
|
||||
|
||||
### 6.2 隐私政策模板(摘要)
|
||||
> “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.”
|
||||
|
||||
(完整模板见附件)
|
||||
|
||||
---
|
||||
|
||||
## 七、变现模型
|
||||
|
||||
| 收入来源 | 实现方式 | 预期占比 |
|
||||
|--------|--------|--------|
|
||||
| 激励视频广告 | 完成专注后解锁主题 | 70% |
|
||||
| 插屏广告 | 每3次专注展示1次(可跳过) | 20% |
|
||||
| 去广告内购 | $2.99 一次性购买 | 10% |
|
||||
| 主题包(未来) | $0.99 解锁新皮肤 | 增量 |
|
||||
|
||||
### 收益预估(1万下载,10% DAU = 1000人):
|
||||
- 日收入 ≈ $3–5
|
||||
- 月收入 ≈ $90–150(初期),随留存提升可翻倍
|
||||
|
||||
---
|
||||
|
||||
## 八、推广策略(零预算冷启动)
|
||||
|
||||
1. **Reddit 渗透**
|
||||
- 发帖 r/ADHD:“Made a focus app that doesn’t 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 版,换取真实测评
|
||||
|
||||
---
|
||||
|
||||
> **愿景**:
|
||||
> 让每一个“不同大脑”的人,都能在专注的路上,被温柔以待。
|
||||
|
||||
---
|
||||
|
||||
> ✨ **备注**:本方案专为个人开发者设计,强调最小可行、快速验证、情感价值优先。
|
||||
# FocusBuddy 产品设计文档
|
||||
|
||||
> **产品名称**:FocusBuddy
|
||||
> **定位**:一款为神经多样性人群设计的、无惩罚、情感支持型专注工具
|
||||
> **目标**:帮助用户温柔地回到当下,而非追求“高效”
|
||||
> **适用平台**:iOS + Android(Flutter 跨平台)
|
||||
> **开发状态**:已完成 MVP 版本
|
||||
> **最后更新**:2025年11月27日
|
||||
|
||||
---
|
||||
|
||||
## 一、产品背景与市场机会
|
||||
|
||||
### 1.1 用户痛点
|
||||
- ADHD 及注意力困难人群常因“无法专注”产生自我批评;
|
||||
- 现有番茄钟工具强调“完成”,失败即惩罚(如 Forest 树枯死),加剧焦虑;
|
||||
- 用户需要的是“允许分心 + 温柔回归”的支持机制,而非效率压榨。
|
||||
|
||||
### 1.2 市场验证
|
||||
- 全球约 **4–5% 成年人**存在 ADHD 特征(CHADD 数据);
|
||||
- Reddit r/ADHD 拥有 **超 200 万订阅者**,TikTok #ADHDTips 话题播放量超 **10 亿**;
|
||||
- 竞品如 Tiimo(估值 $1 亿)、Focus Keeper(长期付费榜前列)证明付费意愿强;
|
||||
- **空白点**:缺乏轻量、离线、情绪友好的垂直工具。
|
||||
|
||||
### 1.3 产品优势
|
||||
- 功能聚焦,无需后端;
|
||||
- 开发成本低(纯本地逻辑);
|
||||
- 无广告干扰,用户体验良好;
|
||||
- 社区自传播潜力大。
|
||||
|
||||
---
|
||||
|
||||
## 二、产品定位与原则
|
||||
|
||||
### 2.1 核心理念
|
||||
> “专注不是坚持不走神,而是每次走神后,都愿意轻轻回来。”
|
||||
|
||||
### 2.2 三大设计原则
|
||||
| 原则 | 说明 |
|
||||
|------|------|
|
||||
| **无惩罚机制** | 分心不中断计时,不断连成就,不重置进度 |
|
||||
| **本地优先** | 所有数据仅存于设备,不联网、不上传 |
|
||||
| **情绪友好** | 用鼓励文案、柔和动效、低刺激视觉降低焦虑 |
|
||||
|
||||
### 2.3 避免踩坑
|
||||
- ❌ 不使用 “ADHD”、“治疗”、“诊断” 等医疗词汇;
|
||||
- ✅ 定位为 “focus support tool for neurodivergent minds”;
|
||||
- ✅ 强调 “gentle”, “kind”, “no guilt”。
|
||||
|
||||
---
|
||||
|
||||
## 三、已实现核心功能
|
||||
|
||||
### 3.1 页面功能
|
||||
|
||||
| 页面 | 功能 | 说明 |
|
||||
|------|------|------|
|
||||
| **Home** | 一键开始专注 | 显示积分卡片、应用标题、时长选择、开始专注按钮和底部导航 |
|
||||
| **Focus** | 专注计时 | 显示计时器、分心按钮和暂停按钮 |
|
||||
| **Complete** | 专注完成 | 显示专注结果、鼓励文案和"Start Another"按钮 |
|
||||
| **History** | 历史记录 | 显示当天记录列表,支持查看详情 |
|
||||
| **Settings** | 设置选项 | 包含默认时长选项、语言选择和隐私政策链接 |
|
||||
| **Profile** | 个人资料 | 显示积分、等级和连续签到记录 |
|
||||
| **Onboarding** | 引导页 | 解释"无惩罚"理念,降低用户困惑 |
|
||||
| **Session Detail** | 会话详情 | 显示单个专注会话的详细信息 |
|
||||
|
||||
### 3.2 核心功能
|
||||
|
||||
| 功能 | 说明 |
|
||||
|------|------|
|
||||
| **无惩罚机制** | 分心不中断计时,不断连成就,不重置进度 |
|
||||
| **分心记录** | "I got distracted"按钮 + 4种分心分类(社交媒体、被打断、感到压力、走神) |
|
||||
| **温柔鼓励** | 随机显示15条鼓励文案,如"Showing up is half the battle" |
|
||||
| **本地存储** | 使用Hive进行数据存储,所有数据仅存于设备 |
|
||||
| **多语言支持** | 支持14种语言(英语、中文、日语、韩语、西班牙语、德语、法语、葡萄牙语、俄语、印地语、印度尼西亚语、意大利语、阿拉伯语) |
|
||||
| **通知功能** | 后台计时通知,提醒用户正在计时中 |
|
||||
| **积分系统** | 完成专注获得积分,提升等级 |
|
||||
| **提前停止确认** | 点击Stop时友好提示,防止误操作 |
|
||||
| **空状态提示** | History页无数据时引导用户 |
|
||||
|
||||
---
|
||||
|
||||
## 四、UI/UX 设计
|
||||
|
||||
### 4.1 视觉风格
|
||||
- **色彩**:莫兰迪色系(主色 `#A7C4BC`,背景 `#F8F6F2`)
|
||||
- **字体**:Nunito(圆润、易读)
|
||||
- **图标**:简洁、清晰的 Material Design 图标
|
||||
- **动效**:柔和的过渡动画,避免快速、刺激的动效
|
||||
|
||||
### 4.2 核心页面设计
|
||||
|
||||
#### 页面 1:Home Screen
|
||||
- 顶部显示积分卡片,包含积分、等级和连续签到记录
|
||||
- 中间显示应用标题和时长选择
|
||||
- 底部显示开始专注按钮和导航栏(历史、设置)
|
||||
|
||||
#### 页面 2:Focus Screen
|
||||
- 中央显示大字体计时器
|
||||
- 下方显示"I got distracted"按钮和暂停按钮
|
||||
- 支持后台计时和通知
|
||||
|
||||
#### 页面 3:Complete Screen
|
||||
- 显示专注结果(时长、分心次数)
|
||||
- 随机显示鼓励文案
|
||||
- 提供"Start Another"按钮
|
||||
|
||||
### 4.3 鼓励文案库
|
||||
存储在 `assets/encouragements.json` 中,包含15条鼓励文案:
|
||||
```json
|
||||
[
|
||||
"Showing up is half the battle.",
|
||||
"Every minute counts.",
|
||||
"You're learning, not failing.",
|
||||
"Gentleness is strength.",
|
||||
"Progress over perfection.",
|
||||
"Your effort matters.",
|
||||
"Small steps, big journey.",
|
||||
"Be kind to your brain.",
|
||||
"You're doing your best.",
|
||||
"One moment at a time.",
|
||||
"Focus is a practice, not a trait.",
|
||||
"It's okay to take breaks.",
|
||||
"You came back — that's what matters.",
|
||||
"Celebrate trying, not just succeeding.",
|
||||
"Your attention is valid."
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、技术实现
|
||||
|
||||
### 5.1 技术栈
|
||||
| 组件 | 方案 |
|
||||
|------|------|
|
||||
| 跨平台框架 | Flutter |
|
||||
| 本地存储 | Hive(加密支持) |
|
||||
| 定时与通知 | flutter_local_notifications |
|
||||
| 权限管理 | permission_handler |
|
||||
| 依赖注入 | get_it |
|
||||
| 国际化 | flutter_localizations + intl |
|
||||
| 字体 | Google Fonts (Nunito) |
|
||||
|
||||
### 5.2 数据结构
|
||||
|
||||
**FocusSession 模型:**
|
||||
```dart
|
||||
class FocusSession {
|
||||
DateTime startTime;
|
||||
DateTime? endTime;
|
||||
int durationMinutes;
|
||||
List<Distraction> distractions;
|
||||
bool isCompleted;
|
||||
}
|
||||
```
|
||||
|
||||
**Distraction 模型:**
|
||||
```dart
|
||||
class Distraction {
|
||||
String type; // e.g., "social", "interrupted", "overwhelmed", "zoned_out"
|
||||
DateTime time;
|
||||
}
|
||||
```
|
||||
|
||||
**UserProgress 模型:**
|
||||
```dart
|
||||
class UserProgress {
|
||||
int totalPoints;
|
||||
int level;
|
||||
int consecutiveCheckIns;
|
||||
bool hasCheckedInToday;
|
||||
List<String> achievements;
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 依赖包
|
||||
|
||||
**核心依赖:**
|
||||
```yaml
|
||||
dependencies:
|
||||
flutter: ^3.10.0-290.4.beta
|
||||
flutter_localizations: ^0.1.0
|
||||
cupertino_icons: ^1.0.8
|
||||
hive: ^2.2.3 # 本地存储
|
||||
hive_flutter: ^1.1.0
|
||||
flutter_local_notifications: ^17.0.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
|
||||
dev_dependencies:
|
||||
flutter_test: ^0.0.0
|
||||
flutter_lints: ^6.0.0
|
||||
hive_generator: ^2.0.0 # Hive代码生成
|
||||
build_runner: ^2.4.0 # 构建工具
|
||||
```
|
||||
|
||||
### 5.4 多语言支持
|
||||
|
||||
已实现14种语言支持:
|
||||
- 英语 (English) 🇬🇧
|
||||
- 中文 (Chinese) 🇨🇳
|
||||
- 日语 (Japanese) 🇯🇵
|
||||
- 韩语 (Korean) 🇰🇷
|
||||
- 西班牙语 (Spanish) 🇪🇸
|
||||
- 德语 (German) 🇩🇪
|
||||
- 法语 (French) 🇫🇷
|
||||
- 葡萄牙语 (Portuguese) 🇧🇷
|
||||
- 俄语 (Russian) 🇷🇺
|
||||
- 印地语 (Hindi) 🇮🇳
|
||||
- 印度尼西亚语 (Indonesian) 🇮🇩
|
||||
- 意大利语 (Italian) 🇮🇹
|
||||
- 阿拉伯语 (Arabic) 🇸🇦
|
||||
|
||||
---
|
||||
|
||||
## 六、合规与隐私
|
||||
|
||||
### 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 doesn’t 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 版本开发,可直接上架应用商店。
|
||||
@@ -277,6 +277,14 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
39
pubspec.yaml
39
pubspec.yaml
@@ -27,25 +27,26 @@ environment:
|
||||
# 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
|
||||
# versions available, run `flutter pub outdated`.
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
flutter_localizations:
|
||||
sdk: flutter
|
||||
|
||||
# The following adds the Cupertino Icons font to your application.
|
||||
# Use with the CupertinoIcons class for iOS style icons.
|
||||
cupertino_icons: ^1.0.8
|
||||
|
||||
# MVP Required Dependencies
|
||||
hive: ^2.2.3 # Local storage
|
||||
hive_flutter: ^1.1.0 # Hive Flutter integration
|
||||
flutter_local_notifications: ^17.0.0 # Notifications
|
||||
permission_handler: ^11.0.0 # Runtime permissions (Android 13+)
|
||||
path_provider: ^2.1.0 # File paths
|
||||
shared_preferences: ^2.2.0 # Simple key-value storage (for onboarding)
|
||||
intl: ^0.20.2 # Date formatting and i18n
|
||||
google_fonts: ^6.1.0 # Google Fonts (Nunito)
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
flutter_localizations:
|
||||
sdk: flutter
|
||||
|
||||
# The following adds the Cupertino Icons font to your application.
|
||||
# Use with the CupertinoIcons class for iOS style icons.
|
||||
cupertino_icons: ^1.0.8
|
||||
|
||||
# MVP Required Dependencies
|
||||
hive: ^2.2.3 # Local storage
|
||||
hive_flutter: ^1.1.0 # Hive Flutter integration
|
||||
flutter_local_notifications: ^17.0.0 # Notifications
|
||||
permission_handler: ^11.0.0 # Runtime permissions (Android 13+)
|
||||
path_provider: ^2.1.0 # File paths
|
||||
shared_preferences: ^2.2.0 # Simple key-value storage (for onboarding)
|
||||
intl: ^0.20.2 # Date formatting and i18n
|
||||
google_fonts: ^6.1.0 # Google Fonts (Nunito)
|
||||
get_it: ^7.7.0 # Dependency injection framework
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
79
test/encouragement_service_test.dart
Normal file
79
test/encouragement_service_test.dart
Normal 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));
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -2,8 +2,9 @@
|
||||
|
||||
**Version**: 1.0
|
||||
**Target Platforms**: iOS & Android (responsive)
|
||||
**Framework**: Flutter-friendly
|
||||
**Framework**: Flutter
|
||||
**Design Philosophy**: Calm • Gentle • Accessible • Neurodivergent-Friendly
|
||||
**Implementation Status**: MVP 已完成
|
||||
|
||||
---
|
||||
|
||||
@@ -47,40 +48,40 @@
|
||||
|
||||
## 4. Core Screens
|
||||
|
||||
### 4.1 Home Screen (Start Focus)
|
||||
### 4.1 Home Screen
|
||||
|
||||
**Layout:**
|
||||
```
|
||||
┌─────────────────────────────────┐
|
||||
│ ┌───────────────────────────┐ │
|
||||
│ │ Points Card │ │
|
||||
│ │ ┌──────┬──────┬────────┐ │ │
|
||||
│ │ │⚡ 120│🎖️ Lv2│📅 Check │ │ │
|
||||
│ │ └──────┴──────┴────────┘ │ │
|
||||
│ └───────────────────────────┘ │
|
||||
│ │
|
||||
│ FocusBuddy │ ← App title (24px, centered)
|
||||
│ │
|
||||
│ │
|
||||
│ [ 25 minutes ] │ ← Duration selector (slider below)
|
||||
│ ◀─────────▶ │ ← Slider: 5min - 60min (step: 5)
|
||||
│ [ 25 minutes ] │ ← Duration display (28px)
|
||||
│ │
|
||||
│ │
|
||||
│ ┌───────────────────────┐ │
|
||||
│ │ Start Focusing │ │ ← Primary button (#A7C4BC)
|
||||
│ │ ▶ │ │ ← 56px height, rounded 16px
|
||||
│ │ Start Focusing ▶ │ │ ← Primary button (#A7C4BC)
|
||||
│ └───────────────────────┘ │
|
||||
│ │
|
||||
│ "Tap 'I got distracted' │ ← Helper text (#8A9B9B)
|
||||
│ anytime — no guilt." │ ← 14px, centered
|
||||
│ │
|
||||
│ │
|
||||
│ 📊 History ⚙️ Settings │ ← Bottom navigation (icons only)
|
||||
│ 📊 History ⚙️ Settings │ ← Bottom navigation (text + icons)
|
||||
└─────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Interactions:**
|
||||
- Slider adjusts duration in real-time (haptic feedback on iOS)
|
||||
- "Start Focusing" button: Scale animation (0.95 → 1.0) on press
|
||||
- Transitions to "During Focus" screen with fade-in (300ms)
|
||||
|
||||
**Animation:**
|
||||
- Subtle particle floating in background (Lottie: `calm-particles.json`)
|
||||
- Particles: 5-8 dots, opacity 0.1-0.3, slow drift upward
|
||||
- Points card is tappable, navigates to Profile screen
|
||||
|
||||
---
|
||||
|
||||
@@ -91,21 +92,16 @@
|
||||
┌─────────────────────────────────┐
|
||||
│ │
|
||||
│ 24:37 │ ← Timer (64px, #5B6D6D)
|
||||
│ │ ← Breathing animation (scale 1.0-1.02)
|
||||
│ │
|
||||
│ │
|
||||
│ ┌───────────────────────┐ │
|
||||
│ │ I got distracted │ │ ← Secondary button (#E0E0E0)
|
||||
│ │ 🤚 │ │ ← 48px height, rounded 12px
|
||||
│ └───────────────────────┘ │
|
||||
│ │
|
||||
│ ┌───────────────────────┐ │
|
||||
│ │ ⏸ Pause │ │ ← Tertiary button (outlined)
|
||||
│ └───────────────────────┘ │ ← Border: 1px #A7C4BC
|
||||
│ │
|
||||
│ │
|
||||
│ 🎵 White Noise: Rain ▼ │ ← Dropdown (bottom sheet)
|
||||
│ │
|
||||
└─────────────────────────────────┘
|
||||
```
|
||||
|
||||
@@ -113,7 +109,6 @@
|
||||
- **Timer**: Count-down display, updates every second
|
||||
- **"I got distracted"** → Opens bottom sheet with 4 options
|
||||
- **Pause** → Shows "Resume" button + elapsed time badge
|
||||
- **White Noise** → Bottom sheet: Off / Rain / Keyboard / Forest
|
||||
|
||||
**Bottom Sheet: Distraction Types**
|
||||
```
|
||||
@@ -125,13 +120,11 @@
|
||||
│ 😰 Felt overwhelmed │ ← Option 3
|
||||
│ 💭 Just zoned out │ ← Option 4
|
||||
│ │
|
||||
│ [Skip this time] │ ← Text button (optional)
|
||||
└─────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Feedback after selection:**
|
||||
- Toast message: "It happens. Let's gently come back." (3s)
|
||||
- Soft haptic pulse
|
||||
- Auto-dismiss bottom sheet
|
||||
- Timer continues running
|
||||
|
||||
@@ -143,7 +136,7 @@
|
||||
```
|
||||
┌─────────────────────────────────┐
|
||||
│ │
|
||||
│ ✨ │ ← Success icon (animated)
|
||||
│ ✨ │ ← Success icon
|
||||
│ │
|
||||
│ You focused for │ ← Headline (20px, #5B6D6D)
|
||||
│ 24 minutes │ ← Large number (32px, bold)
|
||||
@@ -156,75 +149,51 @@
|
||||
│ │ the battle." │ │ ← Italic, #8A9B9B
|
||||
│ └─────────────────────────┘ │
|
||||
│ │
|
||||
│ 🎁 Achievement Unlocked! │ ← Conditional (if milestone hit)
|
||||
│ "Calm Cloud" theme │ ← Badge animation
|
||||
│ │
|
||||
│ ┌───────────────────────┐ │
|
||||
│ │ Start Another │ │ ← Primary button
|
||||
│ └───────────────────────┘ │
|
||||
│ │
|
||||
│ [View Full Report] │ ← Text link
|
||||
└─────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Interactions:**
|
||||
- Success icon: Lottie animation (plays once, 2s)
|
||||
- "Start Another" → Resets to Home screen
|
||||
- "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
|
||||
- "Start Another" → Navigates to Home screen
|
||||
- Shows random encouragement message from `assets/encouragements.json`
|
||||
|
||||
---
|
||||
|
||||
### 4.4 History/Report Screen
|
||||
### 4.4 History Screen
|
||||
|
||||
**Layout:**
|
||||
```
|
||||
┌─────────────────────────────────┐
|
||||
│ 📊 Your Focus Journey │ ← Header (24px)
|
||||
│ │
|
||||
│ ┌─ Today ──────────────────┐ │
|
||||
│ │ │ │
|
||||
│ │ Total: 47 mins │ │ ← Daily summary card
|
||||
│ │ Sessions: 2 │ │
|
||||
│ │ Distractions: 3 │ │
|
||||
│ │ │ │
|
||||
│ │ ▓▓▓▓▓░░░░░ 60% │ │ ← Progress bar
|
||||
│ │ (Goal: 75 mins/day) │ │
|
||||
│ └──────────────────────────┘ │
|
||||
│ ┌──────────────────────────┐ │
|
||||
│ │ Today's Summary │ │
|
||||
│ │ Total: 47 mins │ │
|
||||
│ │ Sessions: 2 │ │
|
||||
│ │ Distractions: 3 │ │
|
||||
│ └──────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─ This Week ─────────────┐ │
|
||||
│ │ Mon ■■■ 24 mins │ │ ← Bar chart (simplified)
|
||||
│ │ Tue ■■■■ 32 mins │ │
|
||||
│ │ Wed ■■ 15 mins │ │
|
||||
│ │ Thu ■■■■■ 47 mins ← │ │ ← Today highlighted
|
||||
│ └──────────────────────────┘ │
|
||||
│ ┌──────────────────────────┐ │
|
||||
│ │ Session 1: 25 mins │ │
|
||||
│ │ • 2 distractions │ │
|
||||
│ │ • 10:00 AM - 10:25 AM │ │
|
||||
│ └──────────────────────────┘ │
|
||||
│ │
|
||||
│ 📈 Top Distraction: │
|
||||
│ 📱 Social media (60%) │ ← Insight card
|
||||
│ │
|
||||
│ [Export PDF Report] │ ← Secondary button (outlined)
|
||||
│ ┌──────────────────────────┐ │
|
||||
│ │ Session 2: 22 mins │ │
|
||||
│ │ • 1 distraction │ │
|
||||
│ │ • 11:00 AM - 11:22 AM │ │
|
||||
│ └──────────────────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Interactions:**
|
||||
- Pull-to-refresh: Animates header particles
|
||||
- Bar chart: Tap day → Shows session details
|
||||
- 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"
|
||||
- Tap session card → Navigates to Session Detail screen
|
||||
- Empty state: Shows message "No sessions yet. Start your first focus session!"
|
||||
|
||||
---
|
||||
|
||||
@@ -235,37 +204,99 @@
|
||||
┌─────────────────────────────────┐
|
||||
│ ⚙️ Settings │
|
||||
│ │
|
||||
│ ┌─ Appearance ──────────────┐ │
|
||||
│ │ Theme: Calm Cloud ▼ │ │ ← Dropdown
|
||||
│ │ [Preview] │ │
|
||||
│ │ │ │
|
||||
│ │ 🔓 Unlock More Themes │ │ ← Ad button
|
||||
│ └───────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─ Focus Settings ──────────┐ │
|
||||
│ │ Default Duration: 25 min │ │
|
||||
│ │ White Noise: Rain │ │
|
||||
│ │ Daily Goal: 75 mins │ │
|
||||
│ │ Default Duration: │ │
|
||||
│ │ • 25 minutes (selected) │ │
|
||||
│ │ • 15 minutes │ │
|
||||
│ │ • 5 minutes │ │
|
||||
│ └───────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─ Notifications ───────────┐ │
|
||||
│ │ Focus Reminders [ON] │ │ ← Toggle
|
||||
│ │ Encourage Messages [ON] │ │
|
||||
│ ┌─ Language ────────────────┐ │
|
||||
│ │ English (selected) │ │
|
||||
│ │ 中文 │ │
|
||||
│ │ 日本語 │ │
|
||||
│ │ 한국어 │ │
|
||||
│ │ Español │ │
|
||||
│ │ Deutsch │ │
|
||||
│ │ Français │ │
|
||||
│ │ Português │ │
|
||||
│ │ Русский │ │
|
||||
│ │ हिन्दी │ │
|
||||
│ │ Bahasa Indonesia │ │
|
||||
│ │ Italiano │ │
|
||||
│ │ العربية │ │
|
||||
│ └───────────────────────────┘ │
|
||||
│ │
|
||||
│ 💎 Remove Ads ($2.99) │ ← IAP button (highlighted)
|
||||
│ │
|
||||
│ Privacy Policy │ ← Links (text buttons)
|
||||
│ About FocusBuddy │
|
||||
│ ┌─ About ───────────────────┐ │
|
||||
│ │ Privacy Policy │ │
|
||||
│ │ Terms of Service │ │
|
||||
│ │ Version 1.0.0 │ │
|
||||
│ └───────────────────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Interactions:**
|
||||
- Theme preview: Shows timer screen with selected theme
|
||||
- "Unlock Themes": Shows rewarded ad → Unlocks next theme
|
||||
- IAP button: Opens native purchase dialog
|
||||
- Toggles: Animated switch with haptic feedback
|
||||
- Tap duration option → Updates default duration
|
||||
- Tap language option → Updates app language
|
||||
- Tap links → Opens respective pages
|
||||
|
||||
### 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%
|
||||
```
|
||||
|
||||
**Flutter Example:**
|
||||
**Flutter Implementation:**
|
||||
```dart
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Color(0xFFA7C4BC),
|
||||
backgroundColor: AppColors.primary,
|
||||
minimumSize: Size(double.infinity, 56),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
elevation: 4,
|
||||
),
|
||||
child: Text('Start Focusing'),
|
||||
child: Text('Start Focusing', style: AppTextStyles.buttonText),
|
||||
onPressed: () {},
|
||||
)
|
||||
```
|
||||
|
||||
@@ -329,25 +361,12 @@ Pressed: background #D5D5D5
|
||||
- Color: `#5B6D6D`
|
||||
- Letter spacing: 2px (monospace feel)
|
||||
|
||||
**Animation:**
|
||||
- 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:**
|
||||
**Flutter Implementation:**
|
||||
```dart
|
||||
AnimatedScale(
|
||||
scale: _breathingAnimation.value,
|
||||
duration: Duration(seconds: 4),
|
||||
curve: Curves.easeInOut,
|
||||
child: Text(
|
||||
'24:37',
|
||||
style: TextStyle(
|
||||
fontSize: 64,
|
||||
fontWeight: FontWeight.w800,
|
||||
letterSpacing: 2,
|
||||
),
|
||||
),
|
||||
)
|
||||
Text(
|
||||
'24:37',
|
||||
style: AppTextStyles.timerDisplay,
|
||||
),
|
||||
```
|
||||
|
||||
---
|
||||
@@ -370,74 +389,59 @@ AnimatedScale(
|
||||
- Slide up: 300ms ease-out
|
||||
- Backdrop: Fade to 0.5 opacity black
|
||||
|
||||
---
|
||||
|
||||
### 5.5 Achievement Badge
|
||||
### 5.5 Points Card
|
||||
|
||||
**Visual:**
|
||||
```
|
||||
┌─────────────────┐
|
||||
│ 🎁 Unlocked! │ ← Emoji + text (14px)
|
||||
│ │
|
||||
│ Calm Cloud │ ← Theme name (18px Bold)
|
||||
│ ▓▓▓▓▓▓▓▓▓▓ │ ← Preview gradient bar
|
||||
└─────────────────┘
|
||||
```
|
||||
- Background: Gradient from `#A7C4BC1A` to `#A7C4BC0D`
|
||||
- Border: 1px solid `#A7C4BC33`
|
||||
- Border radius: 16px
|
||||
- Padding: 16px
|
||||
- Contains points, level, and check-in status
|
||||
|
||||
**Animation:**
|
||||
- Slide up from bottom: 400ms spring
|
||||
- Shimmer sweep: 2s loop (gradient -100% → +100% X)
|
||||
- Auto-dismiss after 5s (slide down)
|
||||
|
||||
**Colors:**
|
||||
- Background: `#FFFFFF`
|
||||
- Border: 2px `#88C9A1` (success color)
|
||||
- Shadow: 0px 8px 24px rgba(136, 201, 161, 0.4)
|
||||
**Flutter Implementation:**
|
||||
```dart
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
AppColors.primary.withOpacity(0.1),
|
||||
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.1 Loading States
|
||||
### 6.1 Screen Transitions
|
||||
|
||||
**When app launches:**
|
||||
- Logo fade-in: 500ms
|
||||
- Particles appear one by one (staggered 100ms)
|
||||
- Total: 1s to interactive
|
||||
- **Cross-fade**: 300ms ease-in-out for all screen transitions
|
||||
- **No slide transitions** to avoid motion sickness
|
||||
|
||||
**When switching screens:**
|
||||
- Cross-fade: 300ms ease-in-out
|
||||
- No slide transitions (avoid motion sickness)
|
||||
### 6.2 Button Interactions
|
||||
|
||||
---
|
||||
- **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:**
|
||||
- Slider adjustment: `.selection`
|
||||
- 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
|
||||
- **App Launch**: Simple circular progress indicator
|
||||
- **Data Loading**: Skeleton screens for list items
|
||||
|
||||
---
|
||||
|
||||
@@ -565,65 +569,73 @@ AnimatedScale(
|
||||
|
||||
---
|
||||
|
||||
## 11. Implementation Notes
|
||||
## 11. Implementation Details
|
||||
|
||||
### 11.1 Flutter Packages
|
||||
|
||||
**Core Dependencies:**
|
||||
```yaml
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
hive: ^2.2.3 # Local storage
|
||||
flutter: ^3.10.0-290.4.beta
|
||||
flutter_localizations: ^0.1.0
|
||||
cupertino_icons: ^1.0.8
|
||||
hive: ^2.2.3 # 本地存储
|
||||
hive_flutter: ^1.1.0
|
||||
flutter_local_notifications: ^17.0.0
|
||||
workmanager: ^0.5.2 # Background tasks
|
||||
lottie: ^3.0.0 # Animations
|
||||
just_audio: ^0.9.36 # White noise
|
||||
google_mobile_ads: ^4.0.0 # AdMob
|
||||
path_provider: ^2.1.0
|
||||
pdf: ^3.10.0 # Report export
|
||||
flutter_local_notifications: ^17.0.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 # 依赖注入框架
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 11.2 Folder Structure
|
||||
|
||||
```
|
||||
lib/
|
||||
├── main.dart
|
||||
├── screens/
|
||||
│ ├── home_screen.dart
|
||||
│ ├── focus_screen.dart
|
||||
│ ├── complete_screen.dart
|
||||
│ ├── history_screen.dart
|
||||
│ └── settings_screen.dart
|
||||
├── widgets/
|
||||
│ ├── primary_button.dart
|
||||
│ ├── timer_display.dart
|
||||
│ ├── distraction_sheet.dart
|
||||
│ └── achievement_badge.dart
|
||||
├── components/
|
||||
│ ├── control_buttons.dart
|
||||
│ ├── distraction_button.dart
|
||||
│ └── timer_display.dart
|
||||
├── l10n/
|
||||
│ ├── app_en.arb
|
||||
│ ├── app_zh.arb
|
||||
│ └── ... (12 more languages)
|
||||
├── models/
|
||||
│ ├── achievement_config.dart
|
||||
│ ├── distraction_type.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/
|
||||
│ ├── storage_service.dart
|
||||
│ ├── achievement_service.dart
|
||||
│ ├── di.dart
|
||||
│ ├── encouragement_service.dart
|
||||
│ ├── notification_service.dart
|
||||
│ └── audio_service.dart
|
||||
├── theme/
|
||||
│ ├── app_colors.dart
|
||||
│ └── app_text_styles.dart
|
||||
└── assets/
|
||||
├── animations/
|
||||
├── sounds/
|
||||
└── fonts/
|
||||
│ ├── points_service.dart
|
||||
│ ├── service_locator.dart
|
||||
│ └── storage_service.dart
|
||||
└── theme/
|
||||
├── app_colors.dart
|
||||
├── app_text_styles.dart
|
||||
└── app_theme.dart
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 11.3 Theme Definition
|
||||
|
||||
**AppColors Class:**
|
||||
```dart
|
||||
// lib/theme/app_colors.dart
|
||||
class AppColors {
|
||||
static const primary = Color(0xFFA7C4BC);
|
||||
static const background = Color(0xFFF8F6F2);
|
||||
@@ -631,9 +643,17 @@ class AppColors {
|
||||
static const textSecondary = Color(0xFF8A9B9B);
|
||||
static const distractionButton = Color(0xFFE0E0E0);
|
||||
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 {
|
||||
static const appTitle = TextStyle(
|
||||
fontFamily: 'Nunito',
|
||||
@@ -654,24 +674,13 @@ class AppTextStyles {
|
||||
fontFamily: 'Nunito',
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.white,
|
||||
color: AppColors.white,
|
||||
);
|
||||
|
||||
static const bodyText = TextStyle(
|
||||
fontFamily: 'Nunito',
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.normal,
|
||||
color: AppColors.textPrimary,
|
||||
);
|
||||
|
||||
static const helperText = TextStyle(
|
||||
fontFamily: 'Nunito',
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w300,
|
||||
color: AppColors.textSecondary,
|
||||
);
|
||||
// Additional text styles...
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -679,26 +688,25 @@ class AppTextStyles {
|
||||
|
||||
### 12.1 Visual QA
|
||||
|
||||
- [ ] All colors match design system
|
||||
- [ ] Fonts render correctly on iOS/Android
|
||||
- [ ] Animations run at 60fps
|
||||
- [ ] No pixel shifts when rotating
|
||||
- [ ] Safe areas respected on all devices
|
||||
- [x] All colors match design system
|
||||
- [x] Fonts render correctly on iOS/Android
|
||||
- [x] Animations run smoothly
|
||||
- [x] No pixel shifts when rotating
|
||||
- [x] Safe areas respected on all devices
|
||||
|
||||
### 12.2 Interaction QA
|
||||
|
||||
- [ ] Buttons have press states
|
||||
- [ ] Haptics fire at correct moments
|
||||
- [ ] Sound effects play (and respect mute)
|
||||
- [ ] Timer counts down accurately
|
||||
- [ ] Bottom sheet dismisses on backdrop tap
|
||||
- [x] Buttons have press states
|
||||
- [x] Timer counts down accurately
|
||||
- [x] Bottom sheet dismisses on backdrop tap
|
||||
- [x] Settings persist after app restart
|
||||
- [x] Language changes apply immediately
|
||||
|
||||
### 12.3 Accessibility QA
|
||||
|
||||
- [ ] Screen reader announces all elements
|
||||
- [ ] High contrast mode works
|
||||
- [ ] Font scaling doesn't break layout
|
||||
- [ ] Minimum touch target: 44×44 (iOS) / 48×48 (Android)
|
||||
- [x] Screen reader announces all elements
|
||||
- [x] Font scaling doesn't break layout
|
||||
- [x] Minimum touch target: 44×44 (iOS) / 48×48 (Android)
|
||||
|
||||
---
|
||||
|
||||
@@ -730,6 +738,6 @@ Store in `assets/encouragements.json`:
|
||||
|
||||
---
|
||||
|
||||
**Document Status:** ✅ Complete
|
||||
**Last Updated:** November 22, 2025
|
||||
**Next Steps:** Create Figma prototype → Share with ADHD community for feedback
|
||||
**Document Status:** ✅ MVP 已实现
|
||||
**Last Updated:** 2025年11月27日
|
||||
**Next Steps:** 上架应用商店
|
||||
Reference in New Issue
Block a user