From 88ef248a3317566a496830374e0489f8c7e830fd Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Mon, 16 Mar 2026 21:35:54 +0100 Subject: [PATCH] feat(05-02): replace HomeScreen with calendar composition and floating Today button - Rewrite HomeScreen as ConsumerStatefulWidget composing CalendarStrip + CalendarDayList - CalendarStripController wires floating Today button to scroll-strip-to-today animation - FloatingActionButton.extended shows "Heute" + Icons.today only when today is out of viewport - Old overdue/today/tomorrow stacked plan sections and ProgressCard fully removed --- .../home/presentation/home_screen.dart | 402 ++---------------- 1 file changed, 41 insertions(+), 361 deletions(-) diff --git a/lib/features/home/presentation/home_screen.dart b/lib/features/home/presentation/home_screen.dart index a59ca2d..16836a7 100644 --- a/lib/features/home/presentation/home_screen.dart +++ b/lib/features/home/presentation/home_screen.dart @@ -1,22 +1,16 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:go_router/go_router.dart'; -import 'package:household_keeper/features/home/domain/daily_plan_models.dart'; -import 'package:household_keeper/features/home/presentation/daily_plan_providers.dart'; -import 'package:household_keeper/features/home/presentation/daily_plan_task_row.dart'; -import 'package:household_keeper/features/home/presentation/progress_card.dart'; -import 'package:household_keeper/features/tasks/presentation/task_providers.dart'; +import 'package:household_keeper/features/home/presentation/calendar_day_list.dart'; +import 'package:household_keeper/features/home/presentation/calendar_providers.dart'; +import 'package:household_keeper/features/home/presentation/calendar_strip.dart'; import 'package:household_keeper/l10n/app_localizations.dart'; -/// Warm coral/terracotta color for overdue section header. -const _overdueColor = Color(0xFFE07A5F); - -/// The app's primary screen: daily plan showing what's due today, -/// overdue tasks, and a preview of tomorrow. +/// The app's primary screen: a horizontal calendar strip at the top with a +/// day task list below. /// -/// Replaces the former placeholder with a full daily workflow: -/// see what's due, check it off, feel progress. +/// Replaces the former stacked overdue/today/tomorrow daily plan layout. +/// Users navigate by tapping day cards to see that day's tasks. class HomeScreen extends ConsumerStatefulWidget { const HomeScreen({super.key}); @@ -25,365 +19,51 @@ class HomeScreen extends ConsumerStatefulWidget { } class _HomeScreenState extends ConsumerState { - /// Task IDs currently animating out after completion. - final Set _completingTaskIds = {}; + late final CalendarStripController _stripController = + CalendarStripController(); - void _onTaskCompleted(int taskId) { - setState(() { - _completingTaskIds.add(taskId); - }); - ref.read(taskActionsProvider.notifier).completeTask(taskId); - } + /// Whether to show the floating "Heute" button. + /// True when the user has scrolled away from today's card. + bool _showTodayButton = false; @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context); - final theme = Theme.of(context); - final dailyPlan = ref.watch(dailyPlanProvider); - return dailyPlan.when( - loading: () => const Center(child: CircularProgressIndicator()), - error: (error, _) => Center(child: Text(error.toString())), - data: (state) { - // Clean up completing IDs that are no longer in the data - _completingTaskIds.removeWhere((id) => - !state.overdueTasks.any((t) => t.task.id == id) && - !state.todayTasks.any((t) => t.task.id == id)); - - return _buildDailyPlan(context, state, l10n, theme); - }, - ); - } - - Widget _buildDailyPlan( - BuildContext context, - DailyPlanState state, - AppLocalizations l10n, - ThemeData theme, - ) { - // Case a: No tasks at all (user hasn't created any rooms/tasks) - if (state.totalTodayCount == 0 && - state.tomorrowTasks.isEmpty && - state.completedTodayCount == 0) { - return _buildNoTasksState(l10n, theme); - } - - // Case b: All clear -- there WERE tasks today but all are done - if (state.overdueTasks.isEmpty && - state.todayTasks.isEmpty && - state.completedTodayCount > 0 && - state.tomorrowTasks.isEmpty) { - return _buildAllClearState(state, l10n, theme); - } - - // Case c: Nothing today, but stuff tomorrow -- show celebration + tomorrow - if (state.overdueTasks.isEmpty && - state.todayTasks.isEmpty && - state.completedTodayCount == 0 && - state.tomorrowTasks.isNotEmpty) { - return _buildAllClearWithTomorrow(state, l10n, theme); - } - - // Case b extended: all clear with tomorrow tasks - if (state.overdueTasks.isEmpty && - state.todayTasks.isEmpty && - state.completedTodayCount > 0 && - state.tomorrowTasks.isNotEmpty) { - return _buildAllClearWithTomorrow(state, l10n, theme); - } - - // Case d: Normal state -- tasks exist - return _buildNormalState(state, l10n, theme); - } - - /// No tasks at all -- first-run empty state. - Widget _buildNoTasksState(AppLocalizations l10n, ThemeData theme) { - return Center( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 32), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Icons.checklist_rounded, - size: 80, - color: theme.colorScheme.onSurface.withValues(alpha: 0.4), - ), - const SizedBox(height: 24), - Text( - l10n.dailyPlanNoTasks, - style: theme.textTheme.headlineSmall, - textAlign: TextAlign.center, - ), - const SizedBox(height: 8), - Text( - l10n.homeEmptyMessage, - style: theme.textTheme.bodyLarge?.copyWith( - color: theme.colorScheme.onSurface.withValues(alpha: 0.6), - ), - textAlign: TextAlign.center, - ), - const SizedBox(height: 24), - FilledButton.tonal( - onPressed: () => context.go('/rooms'), - child: Text(l10n.homeEmptyAction), - ), - ], - ), - ), - ); - } - - /// All tasks done, no tomorrow tasks -- celebration state. - Widget _buildAllClearState( - DailyPlanState state, - AppLocalizations l10n, - ThemeData theme, - ) { - return Center( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 32), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ProgressCard( - completed: state.completedTodayCount, - total: state.totalTodayCount, - ), - const SizedBox(height: 16), - Icon( - Icons.celebration_outlined, - size: 80, - color: theme.colorScheme.onSurface.withValues(alpha: 0.4), - ), - const SizedBox(height: 24), - Text( - l10n.dailyPlanAllClearTitle, - style: theme.textTheme.headlineSmall, - textAlign: TextAlign.center, - ), - const SizedBox(height: 8), - Text( - l10n.dailyPlanAllClearMessage, - style: theme.textTheme.bodyLarge?.copyWith( - color: theme.colorScheme.onSurface.withValues(alpha: 0.6), - ), - textAlign: TextAlign.center, - ), - ], - ), - ), - ); - } - - /// All clear for today but tomorrow tasks exist. - Widget _buildAllClearWithTomorrow( - DailyPlanState state, - AppLocalizations l10n, - ThemeData theme, - ) { - return ListView( + return Stack( children: [ - ProgressCard( - completed: state.completedTodayCount, - total: state.totalTodayCount, + Column( + children: [ + CalendarStrip( + controller: _stripController, + onTodayVisibilityChanged: (visible) { + setState(() => _showTodayButton = !visible); + }, + ), + const Expanded(child: CalendarDayList()), + ], ), - const SizedBox(height: 16), - Center( - child: Column( - children: [ - Icon( - Icons.celebration_outlined, - size: 80, - color: theme.colorScheme.onSurface.withValues(alpha: 0.4), + if (_showTodayButton) + Positioned( + bottom: 16, + left: 0, + right: 0, + child: Center( + child: FloatingActionButton.extended( + onPressed: () { + final now = DateTime.now(); + final today = DateTime(now.year, now.month, now.day); + ref + .read(selectedDateProvider.notifier) + .selectDate(today); + _stripController.scrollToToday(); + }, + icon: const Icon(Icons.today), + label: Text(l10n.calendarTodayButton), ), - const SizedBox(height: 24), - Text( - l10n.dailyPlanAllClearTitle, - style: theme.textTheme.headlineSmall, - textAlign: TextAlign.center, - ), - const SizedBox(height: 8), - Text( - l10n.dailyPlanAllClearMessage, - style: theme.textTheme.bodyLarge?.copyWith( - color: theme.colorScheme.onSurface.withValues(alpha: 0.6), - ), - textAlign: TextAlign.center, - ), - ], + ), ), - ), - const SizedBox(height: 16), - _buildTomorrowSection(state, l10n, theme), ], ); } - - /// Normal state with overdue/today/tomorrow sections. - Widget _buildNormalState( - DailyPlanState state, - AppLocalizations l10n, - ThemeData theme, - ) { - return ListView( - children: [ - ProgressCard( - completed: state.completedTodayCount, - total: state.totalTodayCount, - ), - // Overdue section (conditional) - if (state.overdueTasks.isNotEmpty) ...[ - _buildSectionHeader( - l10n.dailyPlanSectionOverdue, - theme, - color: _overdueColor, - ), - ...state.overdueTasks.map( - (tw) => _buildAnimatedTaskRow(tw, showCheckbox: true), - ), - ], - // Today section - _buildSectionHeader( - l10n.dailyPlanSectionToday, - theme, - color: theme.colorScheme.primary, - ), - if (state.todayTasks.isEmpty) - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: Text( - l10n.dailyPlanAllClearMessage, - style: theme.textTheme.bodyMedium?.copyWith( - color: theme.colorScheme.onSurfaceVariant, - ), - ), - ) - else - ...state.todayTasks.map( - (tw) => _buildAnimatedTaskRow(tw, showCheckbox: true), - ), - // Tomorrow section (conditional, collapsed) - if (state.tomorrowTasks.isNotEmpty) - _buildTomorrowSection(state, l10n, theme), - ], - ); - } - - Widget _buildSectionHeader( - String title, - ThemeData theme, { - required Color color, - }) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: Text( - title, - style: theme.textTheme.titleMedium?.copyWith(color: color), - ), - ); - } - - Widget _buildAnimatedTaskRow( - TaskWithRoom tw, { - required bool showCheckbox, - }) { - final isCompleting = _completingTaskIds.contains(tw.task.id); - - if (isCompleting) { - return _CompletingTaskRow( - key: ValueKey('completing-${tw.task.id}'), - taskWithRoom: tw, - ); - } - - return DailyPlanTaskRow( - key: ValueKey('task-${tw.task.id}'), - taskWithRoom: tw, - showCheckbox: showCheckbox, - onCompleted: () => _onTaskCompleted(tw.task.id), - ); - } - - Widget _buildTomorrowSection( - DailyPlanState state, - AppLocalizations l10n, - ThemeData theme, - ) { - return ExpansionTile( - initiallyExpanded: false, - title: Text( - l10n.dailyPlanUpcomingCount(state.tomorrowTasks.length), - style: theme.textTheme.titleMedium, - ), - children: state.tomorrowTasks - .map( - (tw) => DailyPlanTaskRow( - key: ValueKey('tomorrow-${tw.task.id}'), - taskWithRoom: tw, - showCheckbox: false, - ), - ) - .toList(), - ); - } -} - -/// A task row that animates to zero height on completion. -class _CompletingTaskRow extends StatefulWidget { - const _CompletingTaskRow({ - super.key, - required this.taskWithRoom, - }); - - final TaskWithRoom taskWithRoom; - - @override - State<_CompletingTaskRow> createState() => _CompletingTaskRowState(); -} - -class _CompletingTaskRowState extends State<_CompletingTaskRow> - with SingleTickerProviderStateMixin { - late final AnimationController _controller; - late final Animation _sizeAnimation; - late final Animation _slideAnimation; - - @override - void initState() { - super.initState(); - _controller = AnimationController( - duration: const Duration(milliseconds: 300), - vsync: this, - ); - _sizeAnimation = Tween(begin: 1.0, end: 0.0).animate( - CurvedAnimation(parent: _controller, curve: Curves.easeInOut), - ); - _slideAnimation = Tween( - begin: Offset.zero, - end: const Offset(1.0, 0.0), - ).animate( - CurvedAnimation(parent: _controller, curve: Curves.easeInOut), - ); - _controller.forward(); - } - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return SizeTransition( - sizeFactor: _sizeAnimation, - child: SlideTransition( - position: _slideAnimation, - child: DailyPlanTaskRow( - taskWithRoom: widget.taskWithRoom, - showCheckbox: true, - ), - ), - ); - } }