feat(11-02): render pre-populated tasks with muted visual distinction in calendar UI

- Add isPrePopulated parameter to CalendarTaskRow (default false, backward compat)
- Wrap ListTile in Opacity(0.55) when isPrePopulated to indicate upcoming tasks
- Render 'Demnächst' section in CalendarDayList with muted section header
- Update _buildAnimatedTaskRow to accept and forward isPrePopulated flag
- Update _CompletingTaskRow to accept isPrePopulated (always false during animation)
- Update celebration check to include prePopulatedTasks.isEmpty condition
- Update _completingTaskIds cleanup to also check prePopulatedTasks
This commit is contained in:
2026-04-03 21:26:53 +02:00
parent 9a67c51568
commit 8cbe989aeb
2 changed files with 44 additions and 4 deletions

View File

@@ -58,7 +58,8 @@ class _CalendarDayListState extends ConsumerState<CalendarDayList> {
// Clean up animation IDs for tasks that are no longer in the data. // Clean up animation IDs for tasks that are no longer in the data.
_completingTaskIds.removeWhere((id) => _completingTaskIds.removeWhere((id) =>
!state.overdueTasks.any((t) => t.task.id == id) && !state.overdueTasks.any((t) => t.task.id == id) &&
!state.dayTasks.any((t) => t.task.id == id)); !state.dayTasks.any((t) => t.task.id == id) &&
!state.prePopulatedTasks.any((t) => t.task.id == id));
return _buildContent(context, state, l10n, theme); return _buildContent(context, state, l10n, theme);
}, },
@@ -82,8 +83,12 @@ class _CalendarDayListState extends ConsumerState<CalendarDayList> {
// State (e): Celebration — today is selected and all tasks are done // State (e): Celebration — today is selected and all tasks are done
// (totalTaskCount > 0 so at least some task exists somewhere, but today // (totalTaskCount > 0 so at least some task exists somewhere, but today
// has none remaining after completion). // has none remaining after completion, including no pre-populated tasks).
if (isToday && state.dayTasks.isEmpty && state.overdueTasks.isEmpty && state.totalTaskCount > 0) { if (isToday &&
state.dayTasks.isEmpty &&
state.overdueTasks.isEmpty &&
state.prePopulatedTasks.isEmpty &&
state.totalTaskCount > 0) {
return _buildCelebration(l10n, theme); return _buildCelebration(l10n, theme);
} }
@@ -269,6 +274,24 @@ class _CalendarDayListState extends ConsumerState<CalendarDayList> {
)); ));
} }
// Pre-populated tasks section (upcoming tasks within interval window).
if (state.prePopulatedTasks.isNotEmpty) {
items.add(_buildSectionHeader(
'Demnächst',
theme,
color: theme.colorScheme.onSurface.withValues(alpha: 0.5),
));
for (final tw in state.prePopulatedTasks) {
items.add(_buildAnimatedTaskRow(
tw,
isOverdue: false,
showRoomTag: showRoomTag,
canComplete: true,
isPrePopulated: true,
));
}
}
return ListView(children: items); return ListView(children: items);
} }
@@ -291,6 +314,7 @@ class _CalendarDayListState extends ConsumerState<CalendarDayList> {
required bool isOverdue, required bool isOverdue,
required bool showRoomTag, required bool showRoomTag,
required bool canComplete, required bool canComplete,
bool isPrePopulated = false,
}) { }) {
final isCompleting = _completingTaskIds.contains(tw.task.id); final isCompleting = _completingTaskIds.contains(tw.task.id);
@@ -300,6 +324,7 @@ class _CalendarDayListState extends ConsumerState<CalendarDayList> {
taskWithRoom: tw, taskWithRoom: tw,
isOverdue: isOverdue, isOverdue: isOverdue,
showRoomTag: showRoomTag, showRoomTag: showRoomTag,
isPrePopulated: false, // Completing tasks always show full styling.
); );
} }
@@ -309,6 +334,7 @@ class _CalendarDayListState extends ConsumerState<CalendarDayList> {
isOverdue: isOverdue, isOverdue: isOverdue,
showRoomTag: showRoomTag, showRoomTag: showRoomTag,
canComplete: canComplete, canComplete: canComplete,
isPrePopulated: isPrePopulated,
onCompleted: () => _onTaskCompleted(tw.task.id), onCompleted: () => _onTaskCompleted(tw.task.id),
); );
} }
@@ -321,11 +347,13 @@ class _CompletingTaskRow extends StatefulWidget {
required this.taskWithRoom, required this.taskWithRoom,
required this.isOverdue, required this.isOverdue,
required this.showRoomTag, required this.showRoomTag,
this.isPrePopulated = false,
}); });
final TaskWithRoom taskWithRoom; final TaskWithRoom taskWithRoom;
final bool isOverdue; final bool isOverdue;
final bool showRoomTag; final bool showRoomTag;
final bool isPrePopulated;
@override @override
State<_CompletingTaskRow> createState() => _CompletingTaskRowState(); State<_CompletingTaskRow> createState() => _CompletingTaskRowState();
@@ -372,6 +400,7 @@ class _CompletingTaskRowState extends State<_CompletingTaskRow>
taskWithRoom: widget.taskWithRoom, taskWithRoom: widget.taskWithRoom,
isOverdue: widget.isOverdue, isOverdue: widget.isOverdue,
showRoomTag: widget.showRoomTag, showRoomTag: widget.showRoomTag,
isPrePopulated: widget.isPrePopulated,
onCompleted: () {}, // Already completing — ignore repeat taps. onCompleted: () {}, // Already completing — ignore repeat taps.
), ),
), ),

View File

@@ -14,6 +14,10 @@ const _overdueColor = Color(0xFFE07A5F);
/// ///
/// When [isOverdue] is true the task name uses coral text to visually /// When [isOverdue] is true the task name uses coral text to visually
/// distinguish overdue carry-over from today's regular tasks. /// distinguish overdue carry-over from today's regular tasks.
///
/// When [isPrePopulated] is true the entire row is rendered at 0.55 opacity
/// to indicate it is not yet due (visible within interval window, but due
/// date is in the future).
class CalendarTaskRow extends StatelessWidget { class CalendarTaskRow extends StatelessWidget {
const CalendarTaskRow({ const CalendarTaskRow({
super.key, super.key,
@@ -22,6 +26,7 @@ class CalendarTaskRow extends StatelessWidget {
this.isOverdue = false, this.isOverdue = false,
this.showRoomTag = true, this.showRoomTag = true,
this.canComplete = true, this.canComplete = true,
this.isPrePopulated = false,
}); });
final TaskWithRoom taskWithRoom; final TaskWithRoom taskWithRoom;
@@ -38,12 +43,16 @@ class CalendarTaskRow extends StatelessWidget {
/// When false, the checkbox is disabled (e.g. for future tasks). /// When false, the checkbox is disabled (e.g. for future tasks).
final bool canComplete; final bool canComplete;
/// When true, the row is rendered at 0.55 opacity to indicate an
/// upcoming (not-yet-due) pre-populated task within its interval window.
final bool isPrePopulated;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
final task = taskWithRoom.task; final task = taskWithRoom.task;
return ListTile( final tile = ListTile(
onTap: () => context.go( onTap: () => context.go(
'/rooms/${taskWithRoom.roomId}/tasks/${taskWithRoom.task.id}', '/rooms/${taskWithRoom.roomId}/tasks/${taskWithRoom.task.id}',
), ),
@@ -79,5 +88,7 @@ class CalendarTaskRow extends StatelessWidget {
) )
: null, : null,
); );
return isPrePopulated ? Opacity(opacity: 0.55, child: tile) : tile;
} }
} }