feat(todo): a calendar's feed read, and linked to a project in its payload (ADR 0033, D39)

The developer's decisions of 10 Nov 2026 for calendar feeds, recorded as
D39: a project links to a calendar's iCal feed, its events shown beside
tasks, read-only; each device fetches it, the address sealed in the
project's payload, the last copy kept on the device.

This lays the ground: `readIcal` reads a feed's events (title, start,
end or duration, all-day, place, cancelled, the dates a recurrence
leaves out and the occurrences changed on their own), on the device's
clock through `timezone`, and expands their recurrence with pub.dev's
`rrule` (Apache-2.0, the developer's choice); `ProjectContent` gains
`calendarUrl`, its payload staying schema 1. Nothing shows it yet.

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-10 11:49:34 +01:00
commit 892a58f4fd
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -759,6 +759,15 @@ A comment's handling follows Todoist's (D36), as recorded on 6 Nov 2026. A task'
Reactions are a list of emoji in the comment's payload, each once; a chip's tap takes its reaction off, and with one person on a comment each counts 1. The emoji sheet is Todoist's too, written here: the search above, the recently used, then every category in one list with a bar to jump between them. `emoji_picker_flutter` (MIT) gives only the emoji, their names in each language and the recents, which stay on the device: its own picker has its search at the foot and a tab bar on the SDK's Material. Todo now has the `MaterialUiCompatibilityBridge`, as Learn does, since the editor still imports that library; without it, the picker's tab bar found no localizations.
## Decisions of 10 Nov 2026 (the developer's)
**D39. A project can show a calendar's events, read from its feed by each device.** A project can be linked to a calendar's feed (an iCal address, as Proton Calendar, Nextcloud or a school's timetable share), whose events then show beside tasks, read-only and in the project's colour: in the project's screen (its list and its Calendar), in Today and Upcoming (their lists, agenda and hour grids, all-day events under *All day*), and in filters and labels' results where their days include them. This takes back D36's *Calendar sync: not yet* for reading; nothing is written to a calendar.
- *Fetched by the device, its address sealed* (the agent's, kept by the developer): the address is part of the project's sealed payload (`calendarUrl`), as a feed's address often is its only key, and each device fetches it itself over HTTPS, so the instance neither learns the address nor sees the events; a device keeps the last copy for when it is offline, not synced, as a file in the person's own folder on it (`calendars/<project>.ics`), as their reminders keep their tasks' titles unsealed on the device, and deleted with the link or the project. **Why not through the instance**, as Train's weather is: it would hold everyone's calendar addresses and see their events, for no gain but one fetch instead of one per device.
- *When*: as the app opens, every hour while it is open, and when a list is pulled down (`TaskRefresh`).
- *Read by a parser of our own, recurrence by `rrule`* (the developer's choice of three): the file's events (VEVENT: title, start, end or duration, all-day, place, cancelled, the dates a recurrence leaves out and the occurrences changed) are read by `ical.dart`, short and tested here; RFC 5545's recurrence rules, the large part easy to get wrong, are expanded by pub.dev's `rrule` (Apache-2.0); an event's time zone becomes the device's through `timezone`, already in the lock. **Rejected:** `icalendar_parser` beside `rrule` (one more dependency, less maintained, for the easy part), and recurrence of our own.
- *Shown as a task's date is*: an event becomes a day, a time and a duration on the device's clock, as `TaskDate`, so the lists, the agenda and the grids place it as they place tasks. It has no check, can't be dragged and opens nothing yet but its details.
## Steps
1. **The frame**: go_router and `NavigationFrame` with the four tabs (empty screens that