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.completed) 'completed': 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.sorting != TaskSorting.manual) 'sorting': view.sorting.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),
|
||||
completed: json['completed'] == true,
|
||||
occurrences: json['occurrences'] == true,
|
||||
calendarLayout: named(
|
||||
CalendarLayout.values,
|
||||
'calendarLayout',
|
||||
CalendarLayout.agenda,
|
||||
),
|
||||
grouping: named(TaskGrouping.values, 'grouping', TaskGrouping.none),
|
||||
sorting: named(TaskSorting.values, 'sorting', TaskSorting.manual),
|
||||
date: named(DayRange.values, 'date', DayRange.all),
|
||||
|
|
|
|||
|
|
@ -12,6 +12,14 @@ enum ProjectLayout() {
|
|||
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
|
||||
/// moves as they do, but can't be hidden.
|
||||
enum TaskColumn() {
|
||||
|
|
@ -111,6 +119,9 @@ final class const ProjectView({
|
|||
|
||||
/// Whether a Calendar shows repeating tasks on their later days too.
|
||||
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 TaskSorting sorting = TaskSorting.manual,
|
||||
final DayRange date = DayRange.all,
|
||||
|
|
@ -230,6 +241,7 @@ final class const ProjectView({
|
|||
ProjectLayout? layout,
|
||||
bool? completed,
|
||||
bool? occurrences,
|
||||
CalendarLayout? calendarLayout,
|
||||
TaskGrouping? grouping,
|
||||
TaskSorting? sorting,
|
||||
DayRange? date,
|
||||
|
|
@ -244,6 +256,7 @@ final class const ProjectView({
|
|||
layout: layout ?? this.layout,
|
||||
completed: completed ?? this.completed,
|
||||
occurrences: occurrences ?? this.occurrences,
|
||||
calendarLayout: calendarLayout ?? this.calendarLayout,
|
||||
grouping: grouping ?? this.grouping,
|
||||
sorting: sorting ?? this.sorting,
|
||||
date: date ?? this.date,
|
||||
|
|
@ -261,6 +274,7 @@ final class const ProjectView({
|
|||
layout,
|
||||
completed,
|
||||
occurrences,
|
||||
calendarLayout,
|
||||
grouping,
|
||||
sorting,
|
||||
date,
|
||||
|
|
|
|||
|
|
@ -100,6 +100,37 @@ DisplayTarget projectDisplay(String projectId) {
|
|||
(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)
|
||||
ListTile(
|
||||
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
|
||||
/// what tasks are coloured by, done tasks, future occurrences, sorting
|
||||
/// (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_view.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/model/task.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/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_ui/halkyon_ui.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
|
||||
/// sorts by hand); where it asks, repeating tasks also on their later days.
|
||||
/// *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({
|
||||
required final String projectId,
|
||||
required final ProjectView view,
|
||||
|
|
@ -69,6 +72,23 @@ class const ProjectCalendarView({
|
|||
child: Text(l10n.calendarNoDate),
|
||||
);
|
||||
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(
|
||||
builder: (context, constraints) => constraints.maxWidth >= monthWidth
|
||||
? MonthGrid(
|
||||
|
|
@ -86,13 +106,7 @@ class const ProjectCalendarView({
|
|||
: DayAgenda(
|
||||
projectId: projectId,
|
||||
trailing: noDate,
|
||||
calendarFor: (days) => calendarOf(
|
||||
kept,
|
||||
Day.of(clock.now()),
|
||||
days: days,
|
||||
occurrences: view.occurrences,
|
||||
order: order,
|
||||
),
|
||||
calendarFor: calendarFor,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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_view.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/project_board.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);
|
||||
});
|
||||
|
||||
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', () {
|
||||
/// Another day of today's month, so in its grid.
|
||||
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', () {
|
||||
final read = decodeProject(
|
||||
json({
|
||||
|
|
|
|||
Loading…
Reference in a new issue