feat(todo): the Inbox's Calendar as an Agenda or 3-day
As Todoist's Display in the Inbox: once its layout is Calendar, a row "Layout: Agenda" opens a small choice of Agenda or 3-day; 3-day shows the Inbox's tasks in Upcoming's HourGrid, whatever the window's width, where a tap on an hour adds to the Inbox. Kept as the project's other Display choices, in its sealed view (`calendarLayout`, written only when it isn't the Agenda; a value this version doesn't know reads as the Agenda). Another project's Calendar stays the agenda (or the month), as Todoist offers the choice in the Inbox alone. Whats-New: The Inbox's Calendar can show three days of hours: Display, Layout, 3-day. Whats-New-De: Der Kalender des Eingangs kann drei Tage mit ihren Stunden zeigen: Anzeige, Layout, 3 Tage. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
28e07dbf9d
commit
71b5337d21
6 changed files with 190 additions and 8 deletions
|
|
@ -49,6 +49,8 @@ Map<String, Object?> encodeView(ProjectView view) => {
|
||||||
if (view.layout != ProjectLayout.list) 'layout': view.layout.name,
|
if (view.layout != ProjectLayout.list) 'layout': view.layout.name,
|
||||||
if (view.completed) 'completed': true,
|
if (view.completed) 'completed': true,
|
||||||
if (view.occurrences) 'occurrences': true,
|
if (view.occurrences) 'occurrences': true,
|
||||||
|
if (view.calendarLayout != CalendarLayout.agenda)
|
||||||
|
'calendarLayout': view.calendarLayout.name,
|
||||||
if (view.grouping != TaskGrouping.none) 'grouping': view.grouping.name,
|
if (view.grouping != TaskGrouping.none) 'grouping': view.grouping.name,
|
||||||
if (view.sorting != TaskSorting.manual) 'sorting': view.sorting.name,
|
if (view.sorting != TaskSorting.manual) 'sorting': view.sorting.name,
|
||||||
if (view.date != DayRange.all) 'date': view.date.name,
|
if (view.date != DayRange.all) 'date': view.date.name,
|
||||||
|
|
@ -78,6 +80,11 @@ ProjectView decodeView(Object? json) {
|
||||||
layout: named(ProjectLayout.values, 'layout', ProjectLayout.list),
|
layout: named(ProjectLayout.values, 'layout', ProjectLayout.list),
|
||||||
completed: json['completed'] == true,
|
completed: json['completed'] == true,
|
||||||
occurrences: json['occurrences'] == true,
|
occurrences: json['occurrences'] == true,
|
||||||
|
calendarLayout: named(
|
||||||
|
CalendarLayout.values,
|
||||||
|
'calendarLayout',
|
||||||
|
CalendarLayout.agenda,
|
||||||
|
),
|
||||||
grouping: named(TaskGrouping.values, 'grouping', TaskGrouping.none),
|
grouping: named(TaskGrouping.values, 'grouping', TaskGrouping.none),
|
||||||
sorting: named(TaskSorting.values, 'sorting', TaskSorting.manual),
|
sorting: named(TaskSorting.values, 'sorting', TaskSorting.manual),
|
||||||
date: named(DayRange.values, 'date', DayRange.all),
|
date: named(DayRange.values, 'date', DayRange.all),
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,14 @@ enum ProjectLayout() {
|
||||||
table,
|
table,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// How the Inbox lays out its Calendar (the developer's review, 7 Nov 2026,
|
||||||
|
/// as Todoist's Display offers it in the Inbox alone): day under day, or
|
||||||
|
/// three days of hours side by side, as Upcoming's.
|
||||||
|
enum CalendarLayout() {
|
||||||
|
agenda,
|
||||||
|
threeDay,
|
||||||
|
}
|
||||||
|
|
||||||
/// A column of a project's table (D36); the task's stands among them and
|
/// A column of a project's table (D36); the task's stands among them and
|
||||||
/// moves as they do, but can't be hidden.
|
/// moves as they do, but can't be hidden.
|
||||||
enum TaskColumn() {
|
enum TaskColumn() {
|
||||||
|
|
@ -111,6 +119,9 @@ final class const ProjectView({
|
||||||
|
|
||||||
/// Whether a Calendar shows repeating tasks on their later days too.
|
/// Whether a Calendar shows repeating tasks on their later days too.
|
||||||
final bool occurrences = false,
|
final bool occurrences = false,
|
||||||
|
|
||||||
|
/// How a Calendar lays out its days, the Inbox's alone choosing.
|
||||||
|
final CalendarLayout calendarLayout = CalendarLayout.agenda,
|
||||||
final TaskGrouping grouping = TaskGrouping.none,
|
final TaskGrouping grouping = TaskGrouping.none,
|
||||||
final TaskSorting sorting = TaskSorting.manual,
|
final TaskSorting sorting = TaskSorting.manual,
|
||||||
final DayRange date = DayRange.all,
|
final DayRange date = DayRange.all,
|
||||||
|
|
@ -230,6 +241,7 @@ final class const ProjectView({
|
||||||
ProjectLayout? layout,
|
ProjectLayout? layout,
|
||||||
bool? completed,
|
bool? completed,
|
||||||
bool? occurrences,
|
bool? occurrences,
|
||||||
|
CalendarLayout? calendarLayout,
|
||||||
TaskGrouping? grouping,
|
TaskGrouping? grouping,
|
||||||
TaskSorting? sorting,
|
TaskSorting? sorting,
|
||||||
DayRange? date,
|
DayRange? date,
|
||||||
|
|
@ -244,6 +256,7 @@ final class const ProjectView({
|
||||||
layout: layout ?? this.layout,
|
layout: layout ?? this.layout,
|
||||||
completed: completed ?? this.completed,
|
completed: completed ?? this.completed,
|
||||||
occurrences: occurrences ?? this.occurrences,
|
occurrences: occurrences ?? this.occurrences,
|
||||||
|
calendarLayout: calendarLayout ?? this.calendarLayout,
|
||||||
grouping: grouping ?? this.grouping,
|
grouping: grouping ?? this.grouping,
|
||||||
sorting: sorting ?? this.sorting,
|
sorting: sorting ?? this.sorting,
|
||||||
date: date ?? this.date,
|
date: date ?? this.date,
|
||||||
|
|
@ -261,6 +274,7 @@ final class const ProjectView({
|
||||||
layout,
|
layout,
|
||||||
completed,
|
completed,
|
||||||
occurrences,
|
occurrences,
|
||||||
|
calendarLayout,
|
||||||
grouping,
|
grouping,
|
||||||
sorting,
|
sorting,
|
||||||
date,
|
date,
|
||||||
|
|
|
||||||
|
|
@ -100,6 +100,37 @@ DisplayTarget projectDisplay(String projectId) {
|
||||||
(todo) => write(todo, state, view.copyWith(layout: layout)),
|
(todo) => write(todo, state, view.copyWith(layout: layout)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
// The Inbox's Calendar as an agenda or three days of hours, as
|
||||||
|
// Todoist's Display offers in the Inbox alone (the developer's
|
||||||
|
// review, 7 Nov 2026): a project's quick add from the hours would
|
||||||
|
// add to the Inbox.
|
||||||
|
if (view.layout == ProjectLayout.calendar &&
|
||||||
|
(state.projectOf(projectId)?.inbox ?? false))
|
||||||
|
ListTile(
|
||||||
|
key: calendarLayoutKey,
|
||||||
|
leading: const Icon(LucideIcons.columns3),
|
||||||
|
title: Text(l10n.layoutTitle),
|
||||||
|
subtitle: Text(calendarLayoutText(l10n, view.calendarLayout)),
|
||||||
|
onTap: () async {
|
||||||
|
if (await pickOne(
|
||||||
|
context,
|
||||||
|
l10n.layoutTitle,
|
||||||
|
CalendarLayout.values,
|
||||||
|
view.calendarLayout,
|
||||||
|
(layout) => calendarLayoutText(l10n, layout),
|
||||||
|
)
|
||||||
|
case final layout? when context.mounted) {
|
||||||
|
final now = context.read<TodoCubit>().state;
|
||||||
|
await apply(
|
||||||
|
(todo) => write(
|
||||||
|
todo,
|
||||||
|
now,
|
||||||
|
viewOf(now).copyWith(calendarLayout: layout),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
if (view.layout == ProjectLayout.table)
|
if (view.layout == ProjectLayout.table)
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(LucideIcons.columns3),
|
leading: const Icon(LucideIcons.columns3),
|
||||||
|
|
@ -122,6 +153,16 @@ DisplayTarget projectDisplay(String projectId) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The key of the Inbox's Calendar's row choosing Agenda or 3-day.
|
||||||
|
const calendarLayoutKey = ValueKey('display-calendar-layout');
|
||||||
|
|
||||||
|
/// [layout]'s name, as Upcoming's Display names its own.
|
||||||
|
String calendarLayoutText(AppLocalizations l10n, CalendarLayout layout) =>
|
||||||
|
switch (layout) {
|
||||||
|
CalendarLayout.agenda => l10n.layoutAgenda,
|
||||||
|
CalendarLayout.threeDay => l10n.layoutThreeDay,
|
||||||
|
};
|
||||||
|
|
||||||
/// Upcoming's Display (D17, as Todoist's): agenda or 3-day, in the grid
|
/// Upcoming's Display (D17, as Todoist's): agenda or 3-day, in the grid
|
||||||
/// what tasks are coloured by, done tasks, future occurrences, sorting
|
/// what tasks are coloured by, done tasks, future occurrences, sorting
|
||||||
/// (smart: by time, then priority), and the filters by deadline, priority
|
/// (smart: by time, then priority), and the filters by deadline, priority
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,13 @@ import 'package:halkyon_todo_flutter/l10n/app_localizations.dart';
|
||||||
import 'package:halkyon_todo_flutter/src/feature/project/model/project_calendar.dart';
|
import 'package:halkyon_todo_flutter/src/feature/project/model/project_calendar.dart';
|
||||||
import 'package:halkyon_todo_flutter/src/feature/project/model/project_view.dart';
|
import 'package:halkyon_todo_flutter/src/feature/project/model/project_view.dart';
|
||||||
import 'package:halkyon_todo_flutter/src/feature/project/widget/month_grid.dart';
|
import 'package:halkyon_todo_flutter/src/feature/project/widget/month_grid.dart';
|
||||||
|
import 'package:halkyon_todo_flutter/src/feature/settings/model/todo_settings.dart';
|
||||||
import 'package:halkyon_todo_flutter/src/feature/task/bloc/todo_cubit.dart';
|
import 'package:halkyon_todo_flutter/src/feature/task/bloc/todo_cubit.dart';
|
||||||
import 'package:halkyon_todo_flutter/src/feature/task/model/task.dart';
|
import 'package:halkyon_todo_flutter/src/feature/task/model/task.dart';
|
||||||
import 'package:halkyon_todo_flutter/src/feature/task/model/task_views.dart';
|
import 'package:halkyon_todo_flutter/src/feature/task/model/task_views.dart';
|
||||||
import 'package:halkyon_todo_flutter/src/feature/task/widget/task_tile.dart';
|
import 'package:halkyon_todo_flutter/src/feature/task/widget/task_tile.dart';
|
||||||
import 'package:halkyon_todo_flutter/src/tabs/day_agenda.dart';
|
import 'package:halkyon_todo_flutter/src/tabs/day_agenda.dart';
|
||||||
|
import 'package:halkyon_todo_flutter/src/tabs/hour_grid.dart';
|
||||||
import 'package:halkyon_todo_flutter/src/tabs/week_strip.dart';
|
import 'package:halkyon_todo_flutter/src/tabs/week_strip.dart';
|
||||||
import 'package:halkyon_ui/halkyon_ui.dart';
|
import 'package:halkyon_ui/halkyon_ui.dart';
|
||||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||||
|
|
@ -19,7 +21,8 @@ import 'package:material_ui/material_ui.dart';
|
||||||
/// Display keeps, sub-tasks too, a day's in its sorting (by time where it
|
/// Display keeps, sub-tasks too, a day's in its sorting (by time where it
|
||||||
/// sorts by hand); where it asks, repeating tasks also on their later days.
|
/// sorts by hand); where it asks, repeating tasks also on their later days.
|
||||||
/// *No date* beside the month opens the tasks without one. From
|
/// *No date* beside the month opens the tasks without one. From
|
||||||
/// [ProjectCalendarView.monthWidth] on, a month instead ([MonthGrid]).
|
/// [ProjectCalendarView.monthWidth] on, a month instead ([MonthGrid]). The
|
||||||
|
/// Inbox's, where its Display asks, three days of hours ([HourGrid]).
|
||||||
class const ProjectCalendarView({
|
class const ProjectCalendarView({
|
||||||
required final String projectId,
|
required final String projectId,
|
||||||
required final ProjectView view,
|
required final ProjectView view,
|
||||||
|
|
@ -69,6 +72,23 @@ class const ProjectCalendarView({
|
||||||
child: Text(l10n.calendarNoDate),
|
child: Text(l10n.calendarNoDate),
|
||||||
);
|
);
|
||||||
final order = view.sorting == TaskSorting.manual ? byTime : view.compare;
|
final order = view.sorting == TaskSorting.manual ? byTime : view.compare;
|
||||||
|
ProjectCalendar calendarFor(int days) => calendarOf(
|
||||||
|
kept,
|
||||||
|
Day.of(clock.now()),
|
||||||
|
days: days,
|
||||||
|
occurrences: view.occurrences,
|
||||||
|
order: order,
|
||||||
|
);
|
||||||
|
// The Inbox's three days of hours, as Upcoming's (the developer's
|
||||||
|
// review, 7 Nov 2026), whatever the width.
|
||||||
|
if (view.calendarLayout == CalendarLayout.threeDay &&
|
||||||
|
(state.projectOf(projectId)?.inbox ?? false)) {
|
||||||
|
return HourGrid(
|
||||||
|
foot: 0,
|
||||||
|
coloring: TaskColoring.priority,
|
||||||
|
calendarFor: calendarFor,
|
||||||
|
);
|
||||||
|
}
|
||||||
return LayoutBuilder(
|
return LayoutBuilder(
|
||||||
builder: (context, constraints) => constraints.maxWidth >= monthWidth
|
builder: (context, constraints) => constraints.maxWidth >= monthWidth
|
||||||
? MonthGrid(
|
? MonthGrid(
|
||||||
|
|
@ -86,13 +106,7 @@ class const ProjectCalendarView({
|
||||||
: DayAgenda(
|
: DayAgenda(
|
||||||
projectId: projectId,
|
projectId: projectId,
|
||||||
trailing: noDate,
|
trailing: noDate,
|
||||||
calendarFor: (days) => calendarOf(
|
calendarFor: calendarFor,
|
||||||
kept,
|
|
||||||
Day.of(clock.now()),
|
|
||||||
days: days,
|
|
||||||
occurrences: view.occurrences,
|
|
||||||
order: order,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ import 'package:halkyon_todo_flutter/src/feature/project/data/project_repository
|
||||||
import 'package:halkyon_todo_flutter/src/feature/project/model/project.dart';
|
import 'package:halkyon_todo_flutter/src/feature/project/model/project.dart';
|
||||||
import 'package:halkyon_todo_flutter/src/feature/project/model/project_view.dart';
|
import 'package:halkyon_todo_flutter/src/feature/project/model/project_view.dart';
|
||||||
import 'package:halkyon_todo_flutter/src/feature/project/model/stored_project.dart';
|
import 'package:halkyon_todo_flutter/src/feature/project/model/stored_project.dart';
|
||||||
|
import 'package:halkyon_todo_flutter/src/feature/project/widget/display_sheet.dart';
|
||||||
import 'package:halkyon_todo_flutter/src/feature/project/widget/month_grid.dart';
|
import 'package:halkyon_todo_flutter/src/feature/project/widget/month_grid.dart';
|
||||||
import 'package:halkyon_todo_flutter/src/feature/project/widget/project_board.dart';
|
import 'package:halkyon_todo_flutter/src/feature/project/widget/project_board.dart';
|
||||||
import 'package:halkyon_todo_flutter/src/feature/project/widget/project_calendar_view.dart';
|
import 'package:halkyon_todo_flutter/src/feature/project/widget/project_calendar_view.dart';
|
||||||
|
|
@ -1585,6 +1586,87 @@ void main() {
|
||||||
expect(find.byTooltip('Mark as done'), findsOneWidget);
|
expect(find.byTooltip('Mark as done'), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets("the Inbox's Calendar is an Agenda or 3-day, as its "
|
||||||
|
'Display picks, kept in the Inbox; another project offers '
|
||||||
|
"neither (the developer's review, 7 Nov 2026, as Todoist's)", (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
await calendarOf(tester);
|
||||||
|
await seed(
|
||||||
|
tester,
|
||||||
|
TaskContent(
|
||||||
|
title: 'Dentist',
|
||||||
|
projectId: inbox(),
|
||||||
|
date: TaskDate(Day.of(DateTime.now()), time: 10 * 60),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
expect(find.byType(WeekStrip), findsOneWidget);
|
||||||
|
|
||||||
|
await display(tester);
|
||||||
|
expect(
|
||||||
|
find.descendant(
|
||||||
|
of: find.byKey(calendarLayoutKey),
|
||||||
|
matching: find.text('Agenda'),
|
||||||
|
),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
|
await tester.tap(find.byKey(calendarLayoutKey));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.tap(find.widgetWithText(ListTile, '3-day').last);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(
|
||||||
|
find.descendant(
|
||||||
|
of: find.byKey(calendarLayoutKey),
|
||||||
|
matching: find.text('3-day'),
|
||||||
|
),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
|
await done(tester);
|
||||||
|
|
||||||
|
expect(find.byType(HourGrid), findsOneWidget);
|
||||||
|
expect(find.byType(WeekStrip), findsNothing);
|
||||||
|
expect(
|
||||||
|
find.descendant(
|
||||||
|
of: find.byType(HourGrid),
|
||||||
|
matching: find.text('Dentist'),
|
||||||
|
),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
|
final projects = await tester.runAsync(
|
||||||
|
() => ProjectRepository(projectRows, keys).all(),
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
projects!
|
||||||
|
.singleWhere((project) => project.id == inbox())
|
||||||
|
.content
|
||||||
|
?.view
|
||||||
|
.calendarLayout,
|
||||||
|
CalendarLayout.threeDay,
|
||||||
|
);
|
||||||
|
|
||||||
|
// A project of its own: its Calendar is the agenda alone.
|
||||||
|
await tester.runAsync(
|
||||||
|
() => ProjectRepository(projectRows, keys).save(
|
||||||
|
const ProjectContent(
|
||||||
|
name: 'Shopping',
|
||||||
|
position: 1,
|
||||||
|
view: ProjectView(
|
||||||
|
layout: ProjectLayout.calendar,
|
||||||
|
calendarLayout: CalendarLayout.threeDay,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
projectRows.bringRemoteChanges();
|
||||||
|
await goTab(tester, 'Browse');
|
||||||
|
await tester.tap(find.text('Shopping'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.byType(WeekStrip), findsOneWidget);
|
||||||
|
expect(find.byType(HourGrid), findsNothing);
|
||||||
|
await display(tester);
|
||||||
|
expect(find.byKey(calendarLayoutKey), findsNothing);
|
||||||
|
});
|
||||||
|
|
||||||
group('as a month on a wide window', () {
|
group('as a month on a wide window', () {
|
||||||
/// Another day of today's month, so in its grid.
|
/// Another day of today's month, so in its grid.
|
||||||
Day otherDay() {
|
Day otherDay() {
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,30 @@ void main() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
"the Inbox's Calendar keeps its layout, Agenda or 3-day, written "
|
||||||
|
"only where it isn't the Agenda (the developer's review, 7 Nov 2026)",
|
||||||
|
() {
|
||||||
|
const threeDay = ProjectView(
|
||||||
|
layout: ProjectLayout.calendar,
|
||||||
|
calendarLayout: CalendarLayout.threeDay,
|
||||||
|
);
|
||||||
|
expect(encodeView(threeDay), {
|
||||||
|
'layout': 'calendar',
|
||||||
|
'calendarLayout': 'threeDay',
|
||||||
|
});
|
||||||
|
expect(decodeView(encodeView(threeDay)), threeDay);
|
||||||
|
expect(encodeView(const ProjectView(layout: ProjectLayout.calendar)), {
|
||||||
|
'layout': 'calendar',
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
decodeView({'layout': 'calendar', 'calendarLayout': 'week'}),
|
||||||
|
const ProjectView(layout: ProjectLayout.calendar),
|
||||||
|
reason: 'a layout this version does not know: the Agenda',
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
test('a Display value this version does not know reads as at first', () {
|
test('a Display value this version does not know reads as at first', () {
|
||||||
final read = decodeProject(
|
final read = decodeProject(
|
||||||
json({
|
json({
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue