积分、成就系统

This commit is contained in:
ytc1012
2025-11-27 13:37:10 +08:00
parent 0195cdf54b
commit 58f6ec39b7
35 changed files with 7786 additions and 199 deletions

View File

@@ -9,6 +9,8 @@ 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';
@@ -38,6 +40,8 @@ class _FocusScreenState extends State<FocusScreen> with WidgetsBindingObserver {
bool _isInBackground = false;
final NotificationService _notificationService = getIt<NotificationService>();
final StorageService _storageService = getIt<StorageService>();
final PointsService _pointsService = getIt<PointsService>();
final AchievementService _achievementService = getIt<AchievementService>();
@override
void initState() {
@@ -85,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,
@@ -107,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,
@@ -130,7 +136,8 @@ class _FocusScreenState extends State<FocusScreen> with WidgetsBindingObserver {
// Cancel ongoing notification and show completion notification
await _notificationService.cancelOngoingFocusNotification();
await _saveFocusSession(completed: true);
// Calculate points and update user progress
final pointsData = await _saveFocusSession(completed: true);
if (!mounted) return;
@@ -162,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,
),
),
@@ -183,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,
@@ -200,21 +215,36 @@ 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),
),
@@ -223,7 +253,9 @@ class _FocusScreenState extends State<FocusScreen> with WidgetsBindingObserver {
);
}
Future<void> _saveFocusSession({required bool completed}) async {
Future<Map<String, dynamic>> _saveFocusSession({
required bool completed,
}) async {
try {
final actualMinutes = completed
? widget.durationMinutes
@@ -238,9 +270,47 @@ class _FocusScreenState extends State<FocusScreen> with WidgetsBindingObserver {
distractionTypes: _distractions,
);
// 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) {
// Ignore save errors silently
// Return default values on error
return {
'pointsEarned': 0,
'basePoints': 0,
'honestyBonus': 0,
'totalPoints': 0,
'newAchievements': <String>[],
};
}
}
@@ -249,7 +319,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),
@@ -356,7 +429,11 @@ class _FocusScreenState extends State<FocusScreen> with WidgetsBindingObserver {
// Show distraction-specific encouragement toast
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(widget.encouragementService.getRandomMessage(EncouragementType.distraction)),
content: Text(
widget.encouragementService.getRandomMessage(
EncouragementType.distraction,
),
),
duration: const Duration(seconds: 2),
behavior: SnackBarBehavior.floating,
),
@@ -377,9 +454,8 @@ class _FocusScreenState extends State<FocusScreen> with WidgetsBindingObserver {
padding: const EdgeInsets.all(24.0),
child: Column(
children: [
SizedBox(
height: MediaQuery.of(context).size.height * 0.2,
),
SizedBox(height: MediaQuery.of(context).size.height * 0.2),
SizedBox(height: MediaQuery.of(context).size.height * 0.2),
// Timer Display Component
TimerDisplay(remainingSeconds: _remainingSeconds),
@@ -403,10 +479,8 @@ class _FocusScreenState extends State<FocusScreen> with WidgetsBindingObserver {
resumeText: l10n.resume,
stopText: l10n.stopSession,
),
SizedBox(
height: MediaQuery.of(context).size.height * 0.2,
),
SizedBox(height: MediaQuery.of(context).size.height * 0.2),
SizedBox(height: MediaQuery.of(context).size.height * 0.2),
],
),
),