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>
This commit is contained in:
@@ -0,0 +1,303 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import '../../../../core/theme/app_colors.dart';
|
||||
import '../../../../core/widgets/gentle_nudge_card.dart';
|
||||
import '../../../../core/widgets/streak_ring.dart';
|
||||
import '../../../../core/widgets/task_card.dart';
|
||||
import '../../../../features/auth/presentation/bloc/auth_bloc.dart';
|
||||
import '../bloc/task_list_bloc.dart';
|
||||
|
||||
/// Home screen — the task dashboard.
|
||||
///
|
||||
/// Layout:
|
||||
/// - AppBar greeting.
|
||||
/// - Focus Mode prominent card at the top.
|
||||
/// - Today's tasks list (limited to preferredTaskLoad).
|
||||
/// - Streak summary cards (horizontal scroll).
|
||||
/// - Bottom nav: Tasks, Streaks, Time, Settings.
|
||||
class TaskDashboardScreen extends StatefulWidget {
|
||||
const TaskDashboardScreen({super.key});
|
||||
|
||||
@override
|
||||
State<TaskDashboardScreen> createState() => _TaskDashboardScreenState();
|
||||
}
|
||||
|
||||
class _TaskDashboardScreenState extends State<TaskDashboardScreen> {
|
||||
late final TaskListBloc _taskListBloc;
|
||||
int _currentNavIndex = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_taskListBloc = TaskListBloc()..add(const TasksLoaded());
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_taskListBloc.close();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
String _greeting(AuthState authState) {
|
||||
final name = authState is AuthAuthenticated ? authState.user.displayName : 'Friend';
|
||||
return 'Hey $name! What shall we tackle?';
|
||||
}
|
||||
|
||||
void _onNavTap(int index) {
|
||||
switch (index) {
|
||||
case 0:
|
||||
break; // already here
|
||||
case 1:
|
||||
context.go('/streaks');
|
||||
case 2:
|
||||
context.go('/time');
|
||||
case 3:
|
||||
context.go('/settings');
|
||||
}
|
||||
setState(() => _currentNavIndex = index);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return BlocProvider.value(
|
||||
value: _taskListBloc,
|
||||
child: Scaffold(
|
||||
// ── AppBar ───────────────────────────────────────────────
|
||||
appBar: AppBar(
|
||||
title: BlocBuilder<AuthBloc, AuthState>(
|
||||
builder: (context, state) => Text(
|
||||
_greeting(state),
|
||||
style: theme.textTheme.titleMedium,
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.notifications_none_rounded),
|
||||
onPressed: () {},
|
||||
tooltip: 'Notifications',
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// ── Body ─────────────────────────────────────────────────
|
||||
body: RefreshIndicator(
|
||||
onRefresh: () async => _taskListBloc.add(const TasksLoaded()),
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.only(bottom: 100),
|
||||
children: [
|
||||
// Focus Mode card
|
||||
_FocusModeCard(onTap: () => context.go('/focus')),
|
||||
|
||||
// Gentle nudge (shown conditionally)
|
||||
GentleNudgeCard(
|
||||
previousStreak: 7,
|
||||
onStartSmall: () => context.go('/focus'),
|
||||
onDismiss: () {},
|
||||
),
|
||||
|
||||
// ── Streak rings (horizontal scroll) ───────────────
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Text('Your Streaks', style: theme.textTheme.titleMedium),
|
||||
),
|
||||
SizedBox(
|
||||
height: 120,
|
||||
child: ListView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
children: const [
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 6),
|
||||
child: StreakRing(
|
||||
currentCount: 12,
|
||||
graceDaysRemaining: 2,
|
||||
totalGraceDays: 2,
|
||||
label: 'Daily tasks',
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 6),
|
||||
child: StreakRing(
|
||||
currentCount: 5,
|
||||
graceDaysRemaining: 1,
|
||||
totalGraceDays: 2,
|
||||
label: 'Exercise',
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 6),
|
||||
child: StreakRing(
|
||||
currentCount: 0,
|
||||
isFrozen: true,
|
||||
label: 'Frozen',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// ── Today's tasks ──────────────────────────────────
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Text('Today\'s Tasks', style: theme.textTheme.titleMedium),
|
||||
),
|
||||
|
||||
BlocBuilder<TaskListBloc, TaskListState>(
|
||||
builder: (context, state) {
|
||||
if (state is TaskListLoading) {
|
||||
return const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(32),
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (state is TaskListError) {
|
||||
return _EmptyState(
|
||||
icon: Icons.cloud_off_rounded,
|
||||
title: 'Could not load tasks',
|
||||
subtitle: state.message,
|
||||
);
|
||||
}
|
||||
|
||||
if (state is TaskListLoaded) {
|
||||
final tasks = state.tasks;
|
||||
if (tasks.isEmpty) {
|
||||
return const _EmptyState(
|
||||
icon: Icons.check_circle_outline_rounded,
|
||||
title: 'All clear!',
|
||||
subtitle: 'No tasks for today. Enjoy the calm.',
|
||||
);
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: tasks
|
||||
.take(5)
|
||||
.map((t) => TaskCard(
|
||||
task: t,
|
||||
onTap: () => context.go('/task/${t.id}'),
|
||||
onComplete: () =>
|
||||
_taskListBloc.add(TaskCompleted(t.id)),
|
||||
onSkip: () =>
|
||||
_taskListBloc.add(TaskSkipped(t.id)),
|
||||
))
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// ── FAB: create task ─────────────────────────────────────
|
||||
floatingActionButton: FloatingActionButton.extended(
|
||||
onPressed: () => context.go('/task-create'),
|
||||
icon: const Icon(Icons.add_rounded),
|
||||
label: const Text('New Task'),
|
||||
),
|
||||
|
||||
// ── Bottom nav ───────────────────────────────────────────
|
||||
bottomNavigationBar: BottomNavigationBar(
|
||||
currentIndex: _currentNavIndex,
|
||||
onTap: _onNavTap,
|
||||
items: const [
|
||||
BottomNavigationBarItem(icon: Icon(Icons.task_alt_rounded), label: 'Tasks'),
|
||||
BottomNavigationBarItem(icon: Icon(Icons.local_fire_department_rounded), label: 'Streaks'),
|
||||
BottomNavigationBarItem(icon: Icon(Icons.timer_outlined), label: 'Time'),
|
||||
BottomNavigationBarItem(icon: Icon(Icons.settings_rounded), label: 'Settings'),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Focus Mode Card ────────────────────────────────────────────────
|
||||
|
||||
class _FocusModeCard extends StatelessWidget {
|
||||
final VoidCallback onTap;
|
||||
const _FocusModeCard({required this.onTap});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
color: AppColors.primary,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.self_improvement_rounded, size: 40, color: Colors.white),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Focus Mode',
|
||||
style: theme.textTheme.titleLarge?.copyWith(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Just do the next thing. One at a time.',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: Colors.white.withAlpha(200),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Icon(Icons.arrow_forward_rounded, color: Colors.white),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Empty state ────────────────────────────────────────────────────
|
||||
|
||||
class _EmptyState extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String title;
|
||||
final String subtitle;
|
||||
|
||||
const _EmptyState({
|
||||
required this.icon,
|
||||
required this.title,
|
||||
required this.subtitle,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(32),
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(icon, size: 56, color: AppColors.skipped),
|
||||
const SizedBox(height: 12),
|
||||
Text(title, style: theme.textTheme.titleMedium),
|
||||
const SizedBox(height: 4),
|
||||
Text(subtitle, style: theme.textTheme.bodyMedium, textAlign: TextAlign.center),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user