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/storage_service.dart'; import '../services/encouragement_service.dart'; import '../services/notification_service.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 = NotificationService(); @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(); _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, 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: () { Navigator.pop(context); // Close dialog _timer.cancel(); _saveFocusSession(completed: false); Navigator.pushReplacement( context, MaterialPageRoute( builder: (context) => CompleteScreen( focusedMinutes: actualMinutes, distractionCount: _distractions.length, encouragementService: widget.encouragementService, ), ), ); }, child: Text(l10n.yesStop), ), ], ), ); } Future _saveFocusSession({required bool completed}) async { 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 storageService = StorageService(); await storageService.saveFocusSession(session); } 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) { final l10n = AppLocalizations.of(context)!; setState(() { if (type != null) { _distractions.add(type); } }); // Show encouragement toast ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(l10n.distractionEncouragement), 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)!; 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, ), // Timer Display Text( _formatTime(_remainingSeconds), style: AppTextStyles.timerDisplay, ), 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, ), ], ), ), ), // 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, ), ), ), ), ], ), ), ); } }