feat(todo): Today counts its tasks under its title

As Todoist's, Today's app bar says how many tasks it holds, those
overdue and those due today together ("3 tasks"). TabScreen takes an
optional subtitle for it, left out while tasks are being chosen, when
the title counts those instead.

Whats-New: Today says how many tasks are overdue or due today under its title.
Whats-New-De: „Heute“ zeigt unter dem Titel, wie viele Aufgaben überfällig oder heute fällig sind.

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:37:00 +01:00
commit 8a677e0845
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
8 changed files with 94 additions and 3 deletions

View file

@ -9,6 +9,7 @@
"shellTabToday": "Heute",
"shellTabUpcoming": "Demnächst",
"shellTabInbox": "Eingang",
"todayCount": "{count, plural, =1{1 Aufgabe} other{{count} Aufgaben}}",
"browseProfile": "Profil",
"browseProfileHint": "Dein Konto, wie du dich anmeldest, und dein öffentliches Profil",
"shellTabBrowse": "Übersicht",

View file

@ -15,6 +15,15 @@
"shellTabToday": "Today",
"shellTabUpcoming": "Upcoming",
"shellTabInbox": "Inbox",
"todayCount": "{count, plural, =1{1 task} other{{count} tasks}}",
"@todayCount": {
"description": "Under Today's title: how many tasks are overdue or due today.",
"placeholders": {
"count": {
"type": "int"
}
}
},
"browseProfile": "Profile",
"browseProfileHint": "Your account, how you sign in and your public profile",
"shellTabBrowse": "Browse",

View file

@ -154,6 +154,12 @@ abstract class AppLocalizations {
/// **'Inbox'**
String get shellTabInbox;
/// Under Today's title: how many tasks are overdue or due today.
///
/// In en, this message translates to:
/// **'{count, plural, =1{1 task} other{{count} tasks}}'**
String todayCount(int count);
/// No description provided for @browseProfile.
///
/// In en, this message translates to:

View file

@ -38,6 +38,17 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get shellTabInbox => 'Eingang';
@override
String todayCount(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: '$count Aufgaben',
one: '1 Aufgabe',
);
return '$_temp0';
}
@override
String get browseProfile => 'Profil';

View file

@ -38,6 +38,17 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get shellTabInbox => 'Inbox';
@override
String todayCount(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: '$count tasks',
one: '1 task',
);
return '$_temp0';
}
@override
String get browseProfile => 'Profile';

View file

@ -15,6 +15,10 @@ class const TabScreen({
/// The tab's name, in the app bar.
required final String title,
/// A line under [title], such as how many tasks the tab holds (Today's,
/// as Todoist's); hidden while tasks are being chosen.
final String? subtitle,
/// The tab's content.
required final Widget body,
@ -56,9 +60,24 @@ class const TabScreen({
onPressed: selection.end,
)
: null,
title: Text(
choosing ? l10n.selectionCount(selection.ids.length) : title,
),
title: switch (subtitle) {
final subtitle? when !choosing => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title),
Text(
subtitle,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
_ => Text(
choosing ? l10n.selectionCount(selection.ids.length) : title,
),
},
actions: choosing
? const []
: [

View file

@ -38,8 +38,12 @@ class _TodayTabState() extends State<TodayTab> {
final state = context.watch<TodoCubit>().state;
final today = Day.of(clock.now());
final (:overdue, :due) = todayOf(state.tasks, today);
// Overdue and today's counted together, as Todoist's (the developer's
// review, 4 Nov 2026).
final count = overdue.length + due.length;
return TabScreen(
title: l10n.shellTabToday,
subtitle: state.loaded && count > 0 ? l10n.todayCount(count) : null,
actions: const [SelectTasksMenu()],
floatingActionButton: AddTaskButton(
onPressed: () => showQuickAdd(context, day: today),

View file

@ -1848,6 +1848,36 @@ void main() {
expect(find.text('Pay the bill'), findsOneWidget);
});
testWidgets('Today counts what is overdue and due today under its '
"title, as Todoist's; not while tasks are chosen (the developer's "
'review, 4 Nov 2026)', (tester) async {
final today = Day.of(DateTime.now());
await start(tester, usedBefore: true);
expect(find.textContaining('task'), findsNothing, reason: 'none yet');
for (final (title, day) in [
('Pay the bill', today.plus(-2)),
('Water plants', today),
('Call mum', today),
('Pack', today.plus(1)),
]) {
await seed(
tester,
TaskContent(title: title, projectId: inbox(), date: TaskDate(day)),
);
}
final count = find.descendant(
of: find.byType(AppBar),
matching: find.text('3 tasks'),
);
expect(count, findsOneWidget);
await tester.tap(find.byTooltip('More'));
await tester.pumpAndSettle();
await tester.tap(find.text('Select tasks'));
await tester.pumpAndSettle();
expect(count, findsNothing);
});
testWidgets('Today puts what is overdue first; Reschedule moves it to '
'the day picked', (tester) async {
final today = Day.of(DateTime.now());