feat(todo): recurring tasks, set through a picker (ADR 0033, D7)

A task's date may recur: every n days, weeks (on which weekdays),
months (on its day, or its nth or last weekday), years, from its date
or from when it is done, until a day or for n times. Done, it moves on
to the next day after both its date and today, its deadline as far, its
time kept, its done sub-tasks open again and when it was done kept in
its payload; undo writes all of it back. The date sheet offers the rules
that fit the task's day and a rule of the person's own; rows show a
recurring date with its own icon, the task sheet the rule in words.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-11-01 12:15:51 +01:00
commit aa013f5a00
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -8,8 +8,10 @@ import 'package:halkyon_todo_flutter/src/feature/label/data/label_repository.dar
import 'package:halkyon_todo_flutter/src/feature/project/data/project_repository.dart';
import 'package:halkyon_todo_flutter/src/feature/section/data/section_repository.dart';
import 'package:halkyon_todo_flutter/src/feature/task/data/task_repository.dart';
import 'package:halkyon_todo_flutter/src/feature/task/model/repeat.dart';
import 'package:halkyon_todo_flutter/src/feature/task/model/task.dart';
import 'package:halkyon_ui/halkyon_ui.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
import 'package:material_ui/material_ui.dart';
import '../support/memory_content_key_gateway.dart';
@ -382,6 +384,54 @@ void main() {
expect(title.style?.decoration, TextDecoration.lineThrough);
});
testWidgets('a task set to repeat every day, done, comes again '
'tomorrow, when it was done kept', (tester) async {
final today = Day.of(DateTime.now());
await start(tester, usedBefore: true);
await seed(
tester,
TaskContent(
title: 'Stretch',
projectId: inbox(),
date: TaskDate(today),
),
);
await goTab(tester, 'Inbox');
await tester.tap(find.text('Stretch'));
await tester.pumpAndSettle();
await tester.tap(
find.widgetWithIcon(ListTile, LucideIcons.calendar).last,
);
await tester.pumpAndSettle();
await tester.tap(find.widgetWithText(ListTile, 'Repeat'));
await tester.pumpAndSettle();
await tester.tap(find.text('Every day'));
await tester.pumpAndSettle();
expect(find.text('Every day'), findsOneWidget, reason: 'in the sheet');
await tester.tap(find.byTooltip('Close').last);
await tester.pumpAndSettle();
expect(
find.descendant(
of: find.widgetWithText(ListTile, 'Stretch'),
matching: find.byIcon(LucideIcons.repeat),
),
findsOneWidget,
);
await tester.tap(find.byTooltip('Mark as done'));
await tester.pumpAndSettle();
expect(find.text('Stretch'), findsOneWidget, reason: 'still to do');
expect(find.text('Tomorrow'), findsOneWidget);
final stored = (await tester.runAsync(() => tasks().all()))!.single;
expect(
stored.content?.date,
TaskDate(today.plus(1), repeat: const Repeat(RepeatUnit.day)),
);
expect(stored.content?.completions, hasLength(1));
expect(stored.content?.completed, isFalse);
});
testWidgets('Today puts what is overdue first; Reschedule moves it to '
'the day picked', (tester) async {
final today = Day.of(DateTime.now());