fix(todo): a time picked in the date sheet kept however the sheet closes

The date sheet returned a date only from its days and No date: a time or
a duration picked and the sheet then closed by its X, a swipe or a tap
beside it gave nothing back, so the task kept no time. The sheet now
keeps the date the time makes as it is picked, on the task's day or
today where it had none (as Todoist), and pickTaskDate returns it when
the sheet closes without a choice. Quick add uses the same sheet.

Whats-New: A time picked for a task's date is kept, however you close the Date sheet.
Whats-New-De: Eine Uhrzeit für das Datum einer Aufgabe bleibt, wie auch immer du das Blatt „Datum“ schließt.

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-04 20:34:48 +01:00
commit 33917c6042
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
2 changed files with 124 additions and 17 deletions

View file

@ -18,13 +18,25 @@ typedef DateChoice = ({TaskDate? date});
/// calendar, with a time where the person adds one and with it a duration
/// (D18, as Todoist's), recurring as they pick (D7, [pickRepeat]); or
/// none, where [current] has one.
///
/// A time or duration picked counts however the sheet is closed (the
/// developer's review, 4 Nov 2026: closed after a time, it had kept none):
/// on [current]'s day, or today where there was none, as Todoist's.
Future<DateChoice?> pickTaskDate(
BuildContext context, {
TaskDate? current,
}) => showAdaptiveSheet<DateChoice>(
context,
builder: (context) => _DateSheet(current: current),
);
}) async {
final pending = ValueNotifier<DateChoice?>(null);
try {
final chosen = await showAdaptiveSheet<DateChoice>(
context,
builder: (context) => _DateSheet(current: current, pending: pending),
);
return chosen ?? pending.value;
} finally {
pending.dispose();
}
}
/// Asks for a deadline, a day alone: none to clear it, null where the
/// person changed their mind.
@ -39,8 +51,10 @@ Future<({Day? day})?> pickDeadline(BuildContext context, {Day? current}) async {
return picked == null ? null : (day: Day.of(picked));
}
class const _DateSheet({required final TaskDate? current})
extends StatefulWidget {
class const _DateSheet({
required final TaskDate? current,
required final ValueNotifier<DateChoice?> pending,
}) extends StatefulWidget {
@override
State<_DateSheet> createState() => _DateSheetState();
}
@ -50,6 +64,27 @@ class _DateSheetState() extends State<_DateSheet> {
late Duration? _duration = widget.current?.duration;
late final Repeat? _repeat = widget.current?.repeat;
/// Sets [_time] and [_duration] and keeps the date they make for where
/// the sheet is closed without a day picked: [_DateSheet.current]'s, or
/// today's; nothing where there was no date and the time is taken away.
void _setTime(int? time, Duration? duration) {
setState(() {
_time = time;
_duration = time == null ? null : duration;
});
final current = widget.current;
widget.pending.value = current == null && time == null
? null
: (
date: TaskDate(
current?.day ?? Day.of(clock.now()),
time: _time,
duration: _duration,
repeat: _repeat,
),
);
}
void _choose(Day day, {Repeat? repeat}) =>
Navigator.of(context).pop<DateChoice>((
date: TaskDate(
@ -111,10 +146,7 @@ class _DateSheetState() extends State<_DateSheet> {
trailing: switch (_time) {
final time? => InputChip(
label: Text(timeText(context, time)),
onDeleted: () => setState(() {
_time = null;
_duration = null;
}),
onDeleted: () => _setTime(null, null),
),
null => null,
},
@ -129,8 +161,8 @@ class _DateSheetState() extends State<_DateSheet> {
null => const TimeOfDay(hour: 9, minute: 0),
},
);
if (picked != null) {
setState(() => _time = picked.hour * 60 + picked.minute);
if (picked != null && context.mounted) {
_setTime(picked.hour * 60 + picked.minute, _duration);
}
},
),
@ -141,15 +173,17 @@ class _DateSheetState() extends State<_DateSheet> {
trailing: switch (_duration) {
final duration? => InputChip(
label: Text(durationText(l10n, duration)),
onDeleted: () => setState(() => _duration = null),
onDeleted: () => _setTime(_time, null),
),
null => null,
},
onTap: () async {
if (await _pickDuration(context, _duration) case (
:final duration,
)) {
setState(() => _duration = duration);
if (await _pickDuration(context, _duration)
case (
:final duration,
)
when context.mounted) {
_setTime(_time, duration);
}
},
),

View file

@ -1691,6 +1691,79 @@ void main() {
);
});
group('a time picked counts however the date sheet closes (the '
"developer's review, 4 Nov 2026)", () {
testWidgets("in a task's sheet: on its day, or today where it had "
'none, as Todoist', (tester) async {
final today = Day.of(DateTime.now());
final tomorrow = today.plus(1);
await start(tester, usedBefore: true);
await seed(tester, TaskContent(title: 'Stretch', projectId: inbox()));
await seed(
tester,
TaskContent(
title: 'Write',
projectId: inbox(),
date: TaskDate(tomorrow),
),
);
await goTab(tester, 'Inbox');
await tester.tap(find.text('Stretch'));
await tester.pumpAndSettle();
await tester.tap(find.text('No date').last);
await tester.pumpAndSettle();
await tester.tap(find.text('Time'));
await tester.pumpAndSettle();
await tester.tap(find.text('OK'));
await tester.pumpAndSettle();
await tester.tap(find.byTooltip('Close').last);
await tester.pumpAndSettle();
await closeTaskSheet(tester);
await tester.tap(find.text('Write'));
await tester.pumpAndSettle();
await tester.tap(find.text('Tomorrow').last);
await tester.pumpAndSettle();
await tester.tap(find.text('Time'));
await tester.pumpAndSettle();
await tester.tap(find.text('OK'));
await tester.pumpAndSettle();
// Closed by a tap beside it, as the task's sheet.
await tester.tapAt(const Offset(20, 20));
await tester.pumpAndSettle();
final stored = await tester.runAsync(() => tasks().all());
TaskDate? dateOf(String title) =>
stored!.firstWhere((t) => t.content?.title == title).content?.date;
expect(dateOf('Stretch'), TaskDate(today, time: 9 * 60));
expect(dateOf('Write'), TaskDate(tomorrow, time: 9 * 60));
});
testWidgets('in quick add too', (tester) async {
final today = Day.of(DateTime.now());
await start(tester, usedBefore: true);
await goTab(tester, 'Inbox');
await tester.tap(find.byTooltip('Add task'));
await tester.pumpAndSettle();
await tester.enterText(find.byType(TextField).first, 'Call');
await tester.pump();
await tester.tap(find.byTooltip('Date'));
await tester.pumpAndSettle();
await tester.tap(find.text('Time'));
await tester.pumpAndSettle();
await tester.tap(find.text('OK'));
await tester.pumpAndSettle();
await tester.tap(find.byTooltip('Close').last);
await tester.pumpAndSettle();
await tester.tap(find.byTooltip('Add'));
await tester.pumpAndSettle();
final stored = await tester.runAsync(() => tasks().all());
expect(stored!.single.content?.date, TaskDate(today, time: 9 * 60));
});
});
testWidgets("Today names a task's project and section on the line of "
'its date, at its end', (tester) async {
final today = Day.of(DateTime.now());