import 'dart:async'; import 'package:flutter/material.dart'; import '../l10n/app_localizations.dart'; 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 class FocusScreen extends StatefulWidget { final int durationMinutes; final EncouragementService encouragementService; const FocusScreen({ super.key, required this.durationMinutes, required this.encouragementService, }); @override State createState() => _FocusScreenState(); } class _FocusScreenState extends State with WidgetsBindingObserver { late Timer _timer; late int _remainingSeconds; late DateTime _startTime; final List _distractions = []; bool _isPaused = false; bool _isInBackground = false; final NotificationService _notificationService = getIt(); final StorageService _storageService = getIt(); final PointsService _pointsService = getIt(); final AchievementService _achievementService = getIt(); @override void initState() { super.initState(); _remainingSeconds = widget.durationMinutes * 60; _startTime = DateTime.now(); WidgetsBinding.instance.addObserver(this); _startTimer(); } @override void dispose() { WidgetsBinding.instance.removeObserver(this); _timer.cancel(); // Cancel ongoing notification when leaving the screen _notificationService.cancelOngoingFocusNotification(); super.dispose(); } @override void didChangeAppLifecycleState(AppLifecycleState state) { super.didChangeAppLifecycleState(state); switch (state) { case AppLifecycleState.paused: case AppLifecycleState.inactive: case AppLifecycleState.detached: // App went to background _isInBackground = true; if (!_isPaused && _remainingSeconds > 0) { _showBackgroundNotification(); } break; case AppLifecycleState.resumed: // App came back to foreground _isInBackground = false; _notificationService.cancelOngoingFocusNotification(); break; case AppLifecycleState.hidden: break; } } void _showBackgroundNotification() { 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')}'; _notificationService.showOngoingFocusNotification( remainingMinutes: minutes, remainingSeconds: seconds, title: l10n.notificationFocusInProgress, timeRemainingText: l10n.notificationRemaining(timeStr), ); } void _startTimer() { _timer = Timer.periodic(const Duration(seconds: 1), (timer) { if (!_isPaused && _remainingSeconds > 0) { setState(() { _remainingSeconds--; }); // Update background notification every 30 seconds when in background if (_isInBackground && _remainingSeconds > 0) { if (_remainingSeconds % 30 == 0) { 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')}'; _notificationService.updateOngoingFocusNotification( remainingMinutes: minutes, remainingSeconds: seconds, title: l10n.notificationFocusInProgress, timeRemainingText: l10n.notificationRemaining(timeStr), ); } } if (_remainingSeconds == 0) { _onTimerComplete(); } } }); } void _onTimerComplete() async { _timer.cancel(); // Cancel ongoing notification and show completion notification await _notificationService.cancelOngoingFocusNotification(); // Calculate points and update user progress final pointsData = await _saveFocusSession(completed: true); if (!mounted) return; // Send completion notification with localized text final l10n = AppLocalizations.of(context)!; final minuteText = l10n.minutes(widget.durationMinutes); final notificationBody = _distractions.isEmpty ? l10n.notificationFocusCompleteBodyNoDistractions( widget.durationMinutes, minuteText, ) : l10n.notificationFocusCompleteBody( widget.durationMinutes, minuteText, ); await _notificationService.showFocusCompletedNotification( minutes: widget.durationMinutes, distractionCount: _distractions.length, title: l10n.notificationFocusCompleteTitle, body: notificationBody, ); if (!mounted) return; Navigator.pushReplacement( context, MaterialPageRoute( 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, encouragementService: widget.encouragementService, ), ), ); } void _togglePause() { setState(() { _isPaused = !_isPaused; }); // Update notification when paused if (_isPaused && _isInBackground) { _notificationService.cancelOngoingFocusNotification(); } else if (!_isPaused && _isInBackground) { _showBackgroundNotification(); } } 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); showDialog( context: context, builder: (context) => AlertDialog( title: Text(l10n.stopEarly), content: Text( l10n.stopEarlyMessage(actualMinutes, minuteText), style: AppTextStyles.bodyText, ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: Text(l10n.keepGoing), ), TextButton( onPressed: () async { // Close dialog immediately Navigator.pop(context); _timer.cancel(); // 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, encouragementService: widget.encouragementService, ), ), ); } }); } }, child: Text(l10n.yesStop), ), ], ), ); } Future> _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, ); // 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': [], }; } } void _showDistractionSheet() { final l10n = AppLocalizations.of(context)!; // Map distraction types to translations final distractionOptions = [ ( type: DistractionType.phoneNotification, label: l10n.distractionPhoneNotification, ), (type: DistractionType.socialMedia, label: l10n.distractionSocialMedia), (type: DistractionType.thoughts, label: l10n.distractionThoughts), (type: DistractionType.other, label: l10n.distractionOther), ]; showModalBottomSheet( context: context, backgroundColor: AppColors.white, isScrollControlled: true, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(24)), ), builder: (context) { return SafeArea( child: SingleChildScrollView( child: Padding( padding: EdgeInsets.only( left: 24.0, right: 24.0, top: 24.0, bottom: 24.0 + MediaQuery.of(context).viewInsets.bottom, ), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ // Drag handle Center( child: Container( width: 32, height: 4, decoration: BoxDecoration( color: AppColors.distractionButton, borderRadius: BorderRadius.circular(2), ), ), ), const SizedBox(height: 24), // Title Text( l10n.whatPulledYouAway, style: const TextStyle( fontFamily: 'Nunito', fontSize: 18, fontWeight: FontWeight.w600, color: AppColors.textPrimary, ), ), const SizedBox(height: 24), // Distraction options ...distractionOptions.map((option) { return Column( children: [ ListTile( leading: Text( DistractionType.getEmoji(option.type), style: const TextStyle(fontSize: 24), ), title: Text( option.label, style: AppTextStyles.bodyText, ), onTap: () { Navigator.pop(context); _recordDistraction(option.type); }, ), if (option != distractionOptions.last) const Divider(color: AppColors.divider), ], ); }), const SizedBox(height: 16), // Skip button Center( child: TextButton( onPressed: () { Navigator.pop(context); _recordDistraction(null); }, child: Text(l10n.skipThisTime), ), ), ], ), ), ), ); }, ); } void _recordDistraction(String? type) { setState(() { if (type != null) { _distractions.add(type); } }); // Show distraction-specific encouragement toast ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( widget.encouragementService.getRandomMessage( EncouragementType.distraction, ), ), duration: const Duration(seconds: 2), behavior: SnackBarBehavior.floating, ), ); } @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; return Scaffold( backgroundColor: AppColors.background, body: SafeArea( child: Column( children: [ Expanded( child: SingleChildScrollView( 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), // Timer Display Component TimerDisplay(remainingSeconds: _remainingSeconds), const SizedBox(height: 80), // "I got distracted" Button Component DistractionButton( onPressed: _showDistractionSheet, buttonText: l10n.iGotDistracted, ), const SizedBox(height: 16), // Control Buttons Component ControlButtons( isPaused: _isPaused, onTogglePause: _togglePause, onStopEarly: _stopEarly, pauseText: l10n.pause, resumeText: l10n.resume, stopText: l10n.stopSession, ), SizedBox(height: MediaQuery.of(context).size.height * 0.2), SizedBox(height: MediaQuery.of(context).size.height * 0.2), ], ), ), ), ], ), ), ); } }