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>
304 lines
11 KiB
Dart
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),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|