first commit

This commit is contained in:
ytc1012
2025-11-22 18:17:35 +08:00
commit d427916c6a
169 changed files with 15241 additions and 0 deletions

View File

@@ -0,0 +1,358 @@
import 'dart:async';
import 'package:flutter/material.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<FocusScreen> createState() => _FocusScreenState();
}
class _FocusScreenState extends State<FocusScreen> {
late Timer _timer;
late int _remainingSeconds;
late DateTime _startTime;
final List<String> _distractions = [];
bool _isPaused = false;
@override
void initState() {
super.initState();
_remainingSeconds = widget.durationMinutes * 60;
_startTime = DateTime.now();
_startTimer();
}
void _startTimer() {
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (!_isPaused && _remainingSeconds > 0) {
setState(() {
_remainingSeconds--;
});
if (_remainingSeconds == 0) {
_onTimerComplete();
}
}
});
}
void _onTimerComplete() async {
_timer.cancel();
_saveFocusSession(completed: true);
// Send notification
final notificationService = NotificationService();
await notificationService.showFocusCompletedNotification(
minutes: widget.durationMinutes,
distractionCount: _distractions.length,
);
if (!mounted) return;
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => CompleteScreen(
focusedMinutes: widget.durationMinutes,
distractionCount: _distractions.length,
encouragementService: widget.encouragementService,
),
),
);
}
void _togglePause() {
setState(() {
_isPaused = !_isPaused;
});
}
void _stopEarly() {
final actualMinutes = ((widget.durationMinutes * 60 - _remainingSeconds) / 60).floor();
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Stop early?'),
content: Text(
"That's totally fine — you still focused for $actualMinutes ${actualMinutes == 1 ? 'minute' : 'minutes'}!",
style: AppTextStyles.bodyText,
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Keep going'),
),
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: const Text('Yes, stop'),
),
],
),
);
}
Future<void> _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() {
showModalBottomSheet(
context: context,
backgroundColor: AppColors.white,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
),
builder: (context) {
return SafeArea(
child: Padding(
padding: const EdgeInsets.all(24.0),
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
const Text(
'What pulled you away?',
style: TextStyle(
fontFamily: 'Nunito',
fontSize: 18,
fontWeight: FontWeight.w600,
color: AppColors.textPrimary,
),
),
const SizedBox(height: 24),
// Distraction options
...DistractionType.all.map((type) {
return Column(
children: [
ListTile(
leading: Text(
DistractionType.getEmoji(type),
style: const TextStyle(fontSize: 24),
),
title: Text(
DistractionType.getDisplayName(type),
style: AppTextStyles.bodyText,
),
onTap: () {
Navigator.pop(context);
_recordDistraction(type);
},
),
if (type != DistractionType.all.last)
const Divider(color: AppColors.divider),
],
);
}),
const SizedBox(height: 16),
// Skip button
Center(
child: TextButton(
onPressed: () {
Navigator.pop(context);
_recordDistraction(null);
},
child: const Text('Skip this time'),
),
),
],
),
),
);
},
);
}
void _recordDistraction(String? type) {
setState(() {
if (type != null) {
_distractions.add(type);
}
});
// Show encouragement toast
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text("It happens. Let's gently come back."),
duration: 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
void dispose() {
_timer.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColors.background,
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Spacer(),
// 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: [
const Text(
'I got distracted',
style: TextStyle(
fontFamily: 'Nunito',
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
const SizedBox(width: 8),
Text(
'🤚',
style: const 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 ? 'Resume' : 'Pause'),
],
),
),
),
const Spacer(),
// Stop Button (text button at bottom)
TextButton(
onPressed: _stopEarly,
child: const Text(
'Stop session',
style: TextStyle(
color: AppColors.textSecondary,
fontSize: 14,
),
),
),
],
),
),
),
);
}
}