Files
focusflow/lib/features/tasks/presentation/screens/focus_mode_screen.dart
Oracle Public Cloud User 50931d839d Initial scaffold: FocusFlow ADHD Task Manager Flutter app
BLoC/Cubit state management, ADHD-friendly theme (calming teal, no red),
GetIt DI, GoRouter navigation. Screens: task dashboard, focus mode,
task create/detail, streaks, time perception, settings, onboarding, auth.
Custom widgets: TaskCard, RewardPopup, StreakRing, GentleNudgeCard.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 15:53:58 +00:00

392 lines
13 KiB
Dart

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:focusflow_shared/focusflow_shared.dart';
import '../../../../core/theme/app_colors.dart';
import '../../../../core/widgets/reward_popup.dart';
import '../bloc/next_task_cubit.dart';
/// THE core ADHD feature — "Just do the next thing."
///
/// - Single task displayed, nothing else.
/// - Large title text centered.
/// - Energy level indicator.
/// - Estimated time.
/// - Big "Done!" button (green, satisfying).
/// - "Skip" button (smaller, gray, no guilt).
/// - "I need a break" option.
/// - Timer showing elapsed time (gentle, not anxiety-inducing).
/// - On completion: reward popup appears.
/// - After reward: auto-loads next task or shows "All done!" celebration.
class FocusModeScreen extends StatefulWidget {
const FocusModeScreen({super.key});
@override
State<FocusModeScreen> createState() => _FocusModeScreenState();
}
class _FocusModeScreenState extends State<FocusModeScreen> {
late final NextTaskCubit _cubit;
final Stopwatch _stopwatch = Stopwatch();
Timer? _timer;
bool _showReward = false;
int _elapsedSeconds = 0;
@override
void initState() {
super.initState();
_cubit = NextTaskCubit()..loadNext();
_startTimer();
}
void _startTimer() {
_stopwatch.start();
_timer = Timer.periodic(const Duration(seconds: 1), (_) {
if (mounted) {
setState(() => _elapsedSeconds = _stopwatch.elapsed.inSeconds);
}
});
}
void _resetTimer() {
_stopwatch.reset();
setState(() => _elapsedSeconds = 0);
}
@override
void dispose() {
_timer?.cancel();
_stopwatch.stop();
_cubit.close();
super.dispose();
}
String _formatElapsed() {
final minutes = _elapsedSeconds ~/ 60;
final seconds = _elapsedSeconds % 60;
return '${minutes}m ${seconds.toString().padLeft(2, '0')}s so far';
}
Color _energyColor(String level) {
switch (level) {
case 'low':
return AppColors.energyLow;
case 'high':
return AppColors.energyHigh;
default:
return AppColors.energyMedium;
}
}
String _energyEmoji(String level) {
switch (level) {
case 'low':
return 'Low energy';
case 'high':
return 'High energy';
default:
return 'Medium energy';
}
}
Future<void> _onComplete() async {
_stopwatch.stop();
setState(() => _showReward = true);
}
void _dismissReward() {
setState(() => _showReward = false);
_resetTimer();
_stopwatch.start();
_cubit.complete();
}
void _onSkip() {
_resetTimer();
_cubit.skip();
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return BlocProvider.value(
value: _cubit,
child: Scaffold(
backgroundColor: theme.scaffoldBackgroundColor,
appBar: AppBar(
leading: IconButton(
icon: const Icon(Icons.close_rounded),
onPressed: () => context.go('/'),
tooltip: 'Exit focus mode',
),
title: const Text('Focus Mode'),
centerTitle: true,
),
body: Stack(
children: [
BlocBuilder<NextTaskCubit, NextTaskState>(
builder: (context, state) {
if (state is NextTaskLoading) {
return const Center(child: CircularProgressIndicator());
}
if (state is NextTaskError) {
return Center(
child: Padding(
padding: const EdgeInsets.all(32),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.cloud_off_rounded,
size: 56, color: AppColors.skipped),
const SizedBox(height: 12),
Text(state.message,
style: theme.textTheme.bodyLarge,
textAlign: TextAlign.center),
const SizedBox(height: 16),
FilledButton(
onPressed: () => _cubit.loadNext(),
child: const Text('Try again'),
),
],
),
),
);
}
if (state is NextTaskEmpty) {
return _AllDoneView(onGoBack: () => context.go('/'));
}
if (state is NextTaskLoaded) {
return _TaskFocusView(
task: state.task,
elapsed: _formatElapsed(),
energyColor: _energyColor(state.task.energyLevel),
energyLabel: _energyEmoji(state.task.energyLevel),
onComplete: _onComplete,
onSkip: _onSkip,
onBreak: () {
_stopwatch.stop();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Text(
'Take your time. Tap anywhere when you\'re ready.'),
action: SnackBarAction(
label: 'Resume',
onPressed: () => _stopwatch.start(),
),
),
);
},
);
}
return const SizedBox.shrink();
},
),
// ── Reward overlay ───────────────────────────────────
if (_showReward)
Container(
color: Colors.black.withAlpha(120),
child: RewardPopup(
reward: Reward(
id: 'local-${DateTime.now().millisecondsSinceEpoch}',
userId: '',
rewardType: 'points',
magnitude: 10,
title: 'Task complete!',
createdAt: DateTime.now(),
),
onDismiss: _dismissReward,
),
),
],
),
),
);
}
}
// ── Single-task focus view ─────────────────────────────────────────
class _TaskFocusView extends StatelessWidget {
final Task task;
final String elapsed;
final Color energyColor;
final String energyLabel;
final VoidCallback onComplete;
final VoidCallback onSkip;
final VoidCallback onBreak;
const _TaskFocusView({
required this.task,
required this.elapsed,
required this.energyColor,
required this.energyLabel,
required this.onComplete,
required this.onSkip,
required this.onBreak,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 28),
child: Column(
children: [
const Spacer(flex: 2),
// ── Energy indicator ─────────────────────────────────
Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6),
decoration: BoxDecoration(
color: energyColor.withAlpha(30),
borderRadius: BorderRadius.circular(20),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.bolt_rounded, size: 18, color: energyColor),
const SizedBox(width: 4),
Text(energyLabel,
style: TextStyle(
color: energyColor, fontWeight: FontWeight.w600)),
],
),
),
const SizedBox(height: 24),
// ── Task title ───────────────────────────────────────
Text(
task.title,
style: theme.textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.w700,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 12),
// ── Estimated time ───────────────────────────────────
if (task.estimatedMinutes != null)
Text(
'~${task.estimatedMinutes} min estimated',
style: theme.textTheme.bodyLarge?.copyWith(
color: AppColors.primary,
),
),
const SizedBox(height: 20),
// ── Elapsed time (gentle) ────────────────────────────
Text(
elapsed,
style: theme.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w500,
color: AppColors.primary,
),
),
const Spacer(flex: 3),
// ── Done button ──────────────────────────────────────
SizedBox(
width: double.infinity,
height: 64,
child: FilledButton(
onPressed: onComplete,
style: FilledButton.styleFrom(
backgroundColor: AppColors.completed,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
textStyle: theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w700,
color: Colors.white,
),
),
child: const Text('Done!', style: TextStyle(fontSize: 22, color: Colors.white)),
),
),
const SizedBox(height: 12),
// ── Skip button (smaller, gray, no guilt) ────────────
SizedBox(
width: double.infinity,
height: 52,
child: OutlinedButton(
onPressed: onSkip,
style: OutlinedButton.styleFrom(
foregroundColor: AppColors.skipped,
side: const BorderSide(color: AppColors.skipped),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
child: const Text('Skip for now'),
),
),
const SizedBox(height: 8),
// ── Break button ─────────────────────────────────────
TextButton.icon(
onPressed: onBreak,
icon: const Icon(Icons.self_improvement_rounded, size: 20),
label: const Text('I need a break'),
),
const Spacer(),
],
),
),
);
}
}
// ── All done celebration ───────────────────────────────────────────
class _AllDoneView extends StatelessWidget {
final VoidCallback onGoBack;
const _AllDoneView({required this.onGoBack});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Center(
child: Padding(
padding: const EdgeInsets.all(32),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.celebration_rounded, size: 80, color: AppColors.rewardGold),
const SizedBox(height: 20),
Text(
'All done for today!',
style: theme.textTheme.headlineMedium?.copyWith(fontWeight: FontWeight.w700),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
'You showed up and that matters. Enjoy your free time!',
style: theme.textTheme.bodyLarge,
textAlign: TextAlign.center,
),
const SizedBox(height: 28),
FilledButton(
onPressed: onGoBack,
child: const Text('Back to dashboard'),
),
],
),
),
);
}
}