Files
focusflow/lib/features/tasks/presentation/screens/onboarding_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

363 lines
12 KiB
Dart

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import '../../../../core/theme/app_colors.dart';
import '../widgets/energy_selector.dart';
/// ADHD-specific onboarding flow (4 steps).
///
/// 1. Welcome — "Built for brains like yours."
/// 2. How it works — focus mode, rewards, forgiveness.
/// 3. Preferences — daily task load, energy preference, focus session length.
/// 4. Ready — "Let's do this! No pressure."
class OnboardingScreen extends StatefulWidget {
const OnboardingScreen({super.key});
@override
State<OnboardingScreen> createState() => _OnboardingScreenState();
}
class _OnboardingScreenState extends State<OnboardingScreen> {
final PageController _pageController = PageController();
int _currentPage = 0;
// ── Preference values ────────────────────────────────────────────
double _taskLoad = 5;
String _energyPreference = 'medium';
double _focusMinutes = 25;
void _next() {
if (_currentPage < 3) {
_pageController.nextPage(
duration: const Duration(milliseconds: 350),
curve: Curves.easeInOut,
);
} else {
// Onboarding complete — go home.
context.go('/');
}
}
@override
void dispose() {
_pageController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Scaffold(
body: SafeArea(
child: Column(
children: [
// ── Page indicator ────────────────────────────────────
Padding(
padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 28),
child: Row(
children: List.generate(4, (i) {
final active = i <= _currentPage;
return Expanded(
child: Container(
height: 4,
margin: const EdgeInsets.symmetric(horizontal: 3),
decoration: BoxDecoration(
color: active ? AppColors.primary : AppColors.skipped.withAlpha(80),
borderRadius: BorderRadius.circular(2),
),
),
);
}),
),
),
// ── Pages ────────────────────────────────────────────
Expanded(
child: PageView(
controller: _pageController,
onPageChanged: (i) => setState(() => _currentPage = i),
physics: const NeverScrollableScrollPhysics(),
children: [
_WelcomePage(theme: theme),
_HowItWorksPage(theme: theme),
_PreferencesPage(
theme: theme,
taskLoad: _taskLoad,
onTaskLoadChanged: (v) => setState(() => _taskLoad = v),
energyPreference: _energyPreference,
onEnergyChanged: (v) => setState(() => _energyPreference = v),
focusMinutes: _focusMinutes,
onFocusChanged: (v) => setState(() => _focusMinutes = v),
),
_ReadyPage(theme: theme),
],
),
),
// ── Bottom button ────────────────────────────────────
Padding(
padding: const EdgeInsets.fromLTRB(28, 8, 28, 24),
child: FilledButton(
onPressed: _next,
child: Text(_currentPage == 3 ? 'Let\'s go!' : 'Continue'),
),
),
],
),
),
);
}
}
// ── Step 1: Welcome ────────────────────────────────────────────────
class _WelcomePage extends StatelessWidget {
final ThemeData theme;
const _WelcomePage({required this.theme});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 28),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.self_improvement_rounded, size: 80, color: AppColors.primary),
const SizedBox(height: 24),
Text(
'Built for brains like yours',
style: theme.textTheme.headlineMedium?.copyWith(fontWeight: FontWeight.w700),
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
Text(
'FocusFlow is designed with ADHD in mind. '
'No overwhelming lists, no guilt — just gentle support '
'to help you get things done, one task at a time.',
style: theme.textTheme.bodyLarge,
textAlign: TextAlign.center,
),
],
),
);
}
}
// ── Step 2: How it works ───────────────────────────────────────────
class _HowItWorksPage extends StatelessWidget {
final ThemeData theme;
const _HowItWorksPage({required this.theme});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 28),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'How it works',
style: theme.textTheme.headlineMedium?.copyWith(fontWeight: FontWeight.w700),
textAlign: TextAlign.center,
),
const SizedBox(height: 28),
_FeatureRow(
icon: Icons.center_focus_strong_rounded,
color: AppColors.primary,
title: 'Focus Mode',
subtitle: 'One task at a time. No distractions.',
),
const SizedBox(height: 20),
_FeatureRow(
icon: Icons.celebration_rounded,
color: AppColors.secondary,
title: 'Rewards',
subtitle: 'Earn points & celebrations for completing tasks.',
),
const SizedBox(height: 20),
_FeatureRow(
icon: Icons.favorite_rounded,
color: AppColors.tertiary,
title: 'Forgiveness',
subtitle: 'Missed a day? Grace days have your back. No guilt.',
),
],
),
);
}
}
class _FeatureRow extends StatelessWidget {
final IconData icon;
final Color color;
final String title;
final String subtitle;
const _FeatureRow({
required this.icon,
required this.color,
required this.title,
required this.subtitle,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: color.withAlpha(30),
borderRadius: BorderRadius.circular(12),
),
child: Icon(icon, color: color, size: 28),
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600)),
const SizedBox(height: 2),
Text(subtitle, style: theme.textTheme.bodyMedium),
],
),
),
],
);
}
}
// ── Step 3: Preferences ────────────────────────────────────────────
class _PreferencesPage extends StatelessWidget {
final ThemeData theme;
final double taskLoad;
final ValueChanged<double> onTaskLoadChanged;
final String energyPreference;
final ValueChanged<String> onEnergyChanged;
final double focusMinutes;
final ValueChanged<double> onFocusChanged;
const _PreferencesPage({
required this.theme,
required this.taskLoad,
required this.onTaskLoadChanged,
required this.energyPreference,
required this.onEnergyChanged,
required this.focusMinutes,
required this.onFocusChanged,
});
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 28),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 20),
Text(
'Set your preferences',
style: theme.textTheme.headlineMedium?.copyWith(fontWeight: FontWeight.w700),
),
const SizedBox(height: 8),
Text(
'You can always change these later.',
style: theme.textTheme.bodyMedium,
),
const SizedBox(height: 28),
// Daily task load
Text('How many tasks per day feel right?', style: theme.textTheme.titleSmall),
const SizedBox(height: 4),
Row(
children: [
Expanded(
child: Slider(
value: taskLoad,
min: 1,
max: 10,
divisions: 9,
label: taskLoad.round().toString(),
onChanged: onTaskLoadChanged,
),
),
Text(
'${taskLoad.round()}',
style: theme.textTheme.titleLarge?.copyWith(fontWeight: FontWeight.w700),
),
],
),
const SizedBox(height: 24),
// Energy preference
Text('What\'s your usual energy level?', style: theme.textTheme.titleSmall),
const SizedBox(height: 8),
EnergySelector(value: energyPreference, onChanged: onEnergyChanged),
const SizedBox(height: 24),
// Focus session length
Text('Focus session length', style: theme.textTheme.titleSmall),
const SizedBox(height: 4),
Row(
children: [
Expanded(
child: Slider(
value: focusMinutes,
min: 5,
max: 60,
divisions: 11,
label: '${focusMinutes.round()} min',
onChanged: onFocusChanged,
),
),
Text(
'${focusMinutes.round()} min',
style: theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600),
),
],
),
],
),
);
}
}
// ── Step 4: Ready ──────────────────────────────────────────────────
class _ReadyPage extends StatelessWidget {
final ThemeData theme;
const _ReadyPage({required this.theme});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 28),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.rocket_launch_rounded, size: 80, color: AppColors.primary),
const SizedBox(height: 24),
Text(
'You\'re all set!',
style: theme.textTheme.headlineMedium?.copyWith(fontWeight: FontWeight.w700),
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
Text(
'Let\'s do this. No pressure — start with one task '
'and see how it feels. You\'ve got this.',
style: theme.textTheme.bodyLarge,
textAlign: TextAlign.center,
),
],
),
);
}
}