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

304 lines
11 KiB
Dart

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),
],
),
);
}
}