First run was a bell screen followed by an empty lists screen. It is now
welcome → reminders → an offer to connect a CalDAV account, and then
whatever that answer leaves outstanding:
yes → the add-account wizard inline, its three steps reporting their
place in the outer progress via stepOffset/totalSteps
no → a first list, then the offer to export a copy
Both end on how a list adds tasks — a preview picker over the real task row
and quick-add field, so the answer is given by looking. The list step only
appears when the user has no lists at all, which covers a server that held
no task collections and a restored backup alike; the backup step never
shows on the synced branch, where the server is the copy.
The External-store gates are rebuilt on the same shell, and their copy no
longer claims Agendula only shows tasks that OpenTasks or tasks.org stores.
The reminder step drops the same framing. The done flag keeps its old key,
so existing installs are not dragged back through it.
The two destinations were a filled Button over an OutlinedButton, and the
success line said "Exported 1 list" and nothing else.
Now they are an M3 Expressive ButtonGroup — equal-weight tiles that grow and
squeeze their neighbour on press, corners morphing square — and the outcome
carries the SAF Uri it was aimed at, so the receipt is a tinted row naming
the folder or zip, with an Open button when something on the device answers
for it. Failures get the same treatment.
Both launches can fail, and the outer catch named only
ActivityNotFoundException — so a SecurityException from a locked-down
profile escaped the LaunchedEffect and took the app down. The failure
was also ignored: onBrowserLaunched cleared openInBrowser regardless, so
the user sat on "waiting for your browser" with a spinner and no browser
for the rest of the twenty-minute window, on exactly the AOSP and
GrapheneOS devices the Custom Tabs fallback exists for.
A failed launch now says so, and the step offers the password path,
which is the only way forward on such a device.
Fourteen English sentences were built in the ViewModel and rendered
verbatim as fatal, Working.message and both error fields, so they
shipped untranslated to every locale. Three more sites passed through
whatever text the server, the repository or a Throwable produced.
Seventeen in all, and three of the fourteen were added by fixes earlier
on this branch -- which is the argument for a type rather than a rule.
This is the defect Outcome.Cause was introduced to fix, and it only ever
covered the address step. AddAccountMessage does the same for the rest:
a plain Kotlin sealed type in the app, with the resource mapping beside
Cause's in the screen. The state fields carry it, so a literal is now a
compile error rather than something review has to catch. Nothing lints
for this -- HardcodedText reads XML layout attributes and this app has
none -- so the type is the only guard there is.
The passthroughs get causes of their own, following the rule
Outcome.Cause already states: PollResult.Failed carries RATE_LIMITED,
MAINTENANCE or SERVER_ERROR, keeping the 429/503 distinction that
flattening to one message would lose, and CredentialFailed carries
KEYSTORE_REFUSED or NOT_SAVED. Their reason and detail stay for logs and
are never shown. A server's words are untranslatable and often a bare
status line; a Throwable's are worse.
Twenty keys, base locale only -- Weblate owns the rest and picks them up.
The four tests that asserted on English prose now assert on the message,
which for the cross-domain one is stricter: it pins the host into the
argument instead of anywhere in a sentence.
Two gaps left by fc7e520, both found in review.
The claimed path was kept when the claimed host was replaced, though
both come from the generator we had just decided not to trust. A
subdirectory install behind a proxy reports an empty webroot from inside
the container, so /nextcloud was dropped and discovery ran against the
wrong base -- the same dead end, one level down. The origin is now
rebuilt entirely from the poll endpoint, whose own prefix is whatever
precedes index.php/login/v2/poll.
And the mismatch rode out of poll() with nowhere to go. It is carried on
the state rather than the step, because it is learned during the browser
step while the setting it blames is what the user has to go and fix
afterwards -- so it has to outlive the step that discovered it.
Everything here came from running the account flow against a real
Nextcloud rather than from reading the code.
Discovery
- A typed bare origin now gets the RFC 6764 well-known probe. It was
returned as the only candidate, so `https://cloud.example.com` — what
people actually type — was PROPFIND'd against the web UI, answered 405,
and a working Nextcloud reported as "not a CalDAV server".
- A same-host HTTPS→HTTP redirect is put back on TLS instead of refused
(`dav` change 7). A Nextcloud behind a TLS-terminating proxy without
`overwriteprotocol` builds every redirect with http://, including the
/.well-known/caldav hop discovery depends on. Cross-host still throws.
- Outcomes carry a `Cause` the UI translates, not the server's own words.
"HTTP 405 Method Not Allowed" told someone entering an address nothing,
in a language they may not read, from outside strings.xml.
- An IPv6 origin keeps its brackets: `HttpUrl.host` returns "fd00::1", so
the rebuilt origin did not parse and a homelab address came back as
"not an address".
Login Flow v2
- The poll response's scheme is coerced, never refused. Nextcloud returns
the app password exactly once, so throwing there burned a live
credential and left it dangling in the user's device list. The host
mismatch already worked this way; the scheme now matches it.
Accounts
- The accounts screen observes Room and the sign-in state instead of
taking a snapshot, so a sync landing — or a 401 stopping an account —
reaches a screen that is already open.
- A per-account detail screen, and provider identity (`CalDavProvider`)
shared with the quirk table so one list drives both the icon and the
warning.
- The password field masks: floret-kit's `InlineTextField` gained a
visual transformation, since `KeyboardType.Password` only tells the IME
to drop suggestions.
The parts of chunk 5 that a build can verify. What is left needs a device or a
live server, and is listed in docs/SYNC-PLAN.md rather than guessed at.
- Attribution screen in Settings. dav4jvm is vendored, which makes MPL-2.0
§3.2(a) ours rather than a dependency's, so its row points at PROVENANCE.md
next to upstream. Hand-maintained: generators read POM metadata, which
routinely names a non-SPDX licence and a licence URL that 404s.
- Revocation both ways. A 401 marks the account, stops it before the next
request reaches the network, and takes it off the schedule from outside the
worker — Nextcloud throttles then 429s per source IP, so a timer on a dead
app password degrades the user's other clients. On removal, a bounded
best-effort DELETE of the app password, or uninstalling never revokes it.
- Play compliance: docs/PRIVACY.md linked in the app, declaring Collected and
not Shared; an option to delete the account's tasks from the device too;
REQUEST_IGNORE_BATTERY_OPTIMIZATIONS confirmed absent.
- The server trap matrix as far as a protocol mock reaches, with four tests
left @Ignore'd and their reasons written out.
- docs/SYNC-PLAN.md records what moves to floret-kit, so that branch is a file
move rather than a rediscovery.
/code-review high raised 9 findings, all fixed. Three were serious: app-password
revocation was aimed at the principal URL and revoked nothing; opening the app
put accounts a 401 had stopped back on the timer, because KEEP does not keep
cancelled work; and the incremental path advanced the sync token past bodies a
failed multiget never applied. Also: four scalars were emitted twice whenever
their residue copy survived, which the round-trip corpus could not see.
Not done, and needing you: the live server matrix, releaseTest on device, the
restore-onto-a-fresh-device check, cert4android, and MKCALENDAR feature
detection. Chunk 2's on-device review is still outstanding.
RFC 6578 as an optimisation on top of chunk 3's full path, and sync that runs
by itself.
- :caldav gains sync-collection, with invalidation matched on
DAV:valid-sync-token in the body on any 4xx rather than on a status code —
400, 403, 409 and 412 are all used in the wild, and matching the status is
why Thunderbird never recovers from sabre's 403. No DAV:limit (Nextcloud
regressed it to an HTML error page); 507 on our own href is truncation.
- The engine persists the token per page, after the bodies. An iteration cap
and a no-progress guard, since the RFC never requires the token to advance.
The full path runs anyway every 24h: a token the server accepts over a
pruned change log returns 207, zero changes and no error, and the protocol
gives no other way to notice.
- The three membership traps: an unknown removed href is a no-op, a
delete-then-recreate is re-identified from the UID in the body, and a mass
removal is refused in favour of a real listing, because ACL churn looks
exactly like one.
- Scheduling is a PeriodicWorkRequest with a network constraint, expedited
only for the in-app button. getForegroundInfo is implemented unconditionally
(setExpedited falls back to a foreground service below API 31 and the
default throws) but declares no service type, which would have pulled back
the Android 15 dataSync budget and a Play video-demo requirement.
initialIncomplete turned out not to be needed: adopting a token only after a
full reconciliation completes removes the hazard it guarded, so there is
nothing to persist atomically with anything.
/code-review high raised 8 findings, all fixed. The two that mattered: the
cadence clock sat in the backed-up DataStore, so a restore would have made the
engine trust a stale token for a day — both sync-state stores now have their
own excluded file; and the incremental download path lacked the write-phase
guard, overwriting local edits that had never reached the server. Reasoning in
docs/SYNC-PLAN.md.
Chunk 2's on-device review is still outstanding; none of this has run on a
device or against a real server.
Full bidirectional sync, correct but not yet clever: calendar-query with no
time-range, calendar-multiget in batches matched against what was asked for,
conditional writes, and per-resource quarantine so one bad task cannot stop a
collection.
- :caldav gains CalendarCollection (list/fetch/create/update/delete + the
three-way 412 triage), ETag with its weak flag, vdirsyncer-style resource
names, and the RemoteCalendar seam.
- CalDavHttp now sends Accept-Encoding: identity and Prefer: handling=strict,
the two headers that keep ETags strong and our bytes unrepaired.
- :app gains CollectionSyncer (the reconciliation), SyncEngine, SyncStore,
QuarantineStore, SyncReport and a working SyncWorker, plus an in-app sync
trigger since ContentResolver.requestSync is gated at our targetSdk.
- VTimeZones closes a chunk-1 gap: the mapper emitted TZID with no VTIMEZONE
to resolve it, which handling=strict turns from a repair into a rejection.
/code-review high raised 11 findings, all fixed. The three that mattered:
an empty listing swept the whole list (a VTODO comp-filter some servers
mishandle is not proof of deletion, so an empty listing now never sweeps);
quarantine never covered creates, so a permanently rejected new task was
re-PUT forever; and a RELATED-TO deleted on the server was re-uploaded on the
next edit. Reasoning recorded in docs/SYNC-PLAN.md.
Chunk 2's on-device review is still outstanding; nothing here has been run on
a device or against a real server.
One flow, one back-stack entry, as a stepper rather than four destinations —
the steps are not independently reachable, and "back" from the browser step
abandons a server-side flow rather than popping a screen. It hangs off Settings
with the same sliding-section pattern Storage -> Export uses.
address -> discovery -> (Nextcloud browser approval | username + password)
-> pick lists -> add account
Provider warnings come before the attempt, not after: type a Fastmail or iCloud
address and the app password rule is stated while you type, which is the single
most common support ticket a CalDAV client inherits. Google is refused with the
reason. A login-flow host mismatch is shown, not refused — reverse proxies are
ordinary on self-hosted installs.
PreemptiveBasicInterceptor is deleted. The vendored BasicDigestAuthHandler
already sends Basic preemptively over HTTPS, also does Digest (Baikal defaults
to it, OkHttp has none), caches the working scheme, and scopes by registrable
domain — which is what iCloud's cross-host home set needs. Two implementations
of one job is the defect chunk 1 removed from ICalendarWriter.
⚠️ That handler compares its `domain` against the *registrable* domain, so
passing the full host meant credentials were withheld from every request to
every subdomain — i.e. every self-hosted Nextcloud, silently 401ing forever.
Pinned by CalDavHttpTest.
CalDavGateway and AccountCreator put the network and the database behind
interfaces so the sign-in state machine is testable without a server, a
database, a Keystore or an AccountManager. It had no tests, and the review
found eight issues in it.
Account creation is transactional and rolls its lists back explicitly:
account_id is ON DELETE SET NULL, so deleting the row alone leaves orphan
lists behind and every retry adds another set.
The store picker and the export screen were the two frontend surfaces
the own-store work left unbuilt, so both backends shipped unreachable.
Settings gains a Storage section holding them: a full-screen picker over
Own / an installed external provider (dimmed when none is present, named
after the provider's own app), and an export screen with a per-list tick
and the two SAF destinations, a folder or a single zip. The picker asks
for the provider's runtime permission before writing the mode, so a
denial leaves the readable store in place instead of dropping the user on
the gate; a refusal is reported with a route to app settings.
Making the mode switchable at runtime had two consequences:
- reminders are armed off whichever store was active when they were
scheduled, so a switch rebuilds the set. ReminderScheduler.sync() is
now serialised — it is a read-modify-write over ScheduledReminderStore,
and overlapping runs each wrote their own set as the whole truth
- the permission gate is the only screen an External user can reach once
their provider app stops answering, so it offers the way back to our
own store
ExportWriter no longer deletes a previous export before recreating it (a
failure in between lost both), lists the target directory once instead of
per document, and carries a typed ExportFailure so the screen can report
in the user's language rather than an exception message.
Owning the store left a fresh install with no lists and no way to make
one, so no way to save a task. The seam gains updateList/deleteList
beside createLocalList on both paths — the External one addresses the row
as its own account's sync adapter, the only caller the provider lets
write tasklists. ListEditorSheet is the family's full-screen sheet: name
field, a 12-colour palette, and a destructive row behind a confirm when
editing. Entry points are a "New list" row under the home Lists section,
an empty state with a create button, and the home FAB switching to "New
list" while there are none. Deleting takes the list's tasks with it and
is offered only for device-only lists.
Four defects a review of the branch turned up:
- completing one occurrence closed the whole series — setCompleted wrote
the master, the row TaskDao.tasks filters on. setCompletedInstance
forks a RECURRENCE-ID override the way updateInstance does; phase 2
always specified this, only the edit half had it
- the expansion ceiling was spent on the past, so a sub-daily series
stopped expanding months before today and never reached Today or
Upcoming
- an imported START-referenced reminder fired off DUE, because the seam
collapsed alarms to a bare minute count. TaskReminder carries the
anchor now
- registerObserver bound a live flow to whichever store was active at
subscription, so a Settings store switch left every screen listening to
the store it had stopped reading
Makes Codeberg canonical for git, issues, PRs, tags and releases. The self-hosted Gitea instance stays build infrastructure: signing key, F-Droid publishing, release pipeline. Ports the setup Calendula already runs on, adapted where Agendula genuinely differs.
**This PR is its own test.** It is the first PR opened on Codeberg, so a green `CI` check proves the new runner works *and* that the submodule resolves from its new home.
### 1 · Workflows split by directory
Forgejo's lookup is first-match-wins across `.forgejo/` → `.gitea/` → `.github/`, and Gitea cannot see `.forgejo/` at all. So each forge sees exactly one set, with no duplicated files and no expression to keep in sync:
| Directory | Runs on | Contains | Secrets |
| --- | --- | --- | --- |
| `.forgejo/workflows/` | Codeberg | `ci.yaml`, `translations.yaml` | **none** |
| `.gitea/workflows/` | Gitea | `release.yaml`, `renovate.yml` | all of them |
The line is drawn at **secrets, not CI-vs-release** — that is what makes fork PRs safe. Renovate deliberately does *not* move despite opening PRs here; it keeps running where its token already lives and merely talks to Codeberg's API.
CI also gains three fixes: an explicit `SKIP_RE` skip-list that names the build-relevant files in the log, base-ref normalisation, and a fully-qualified `android-actions/setup-android` — Codeberg resolves bare `uses:` refs against `data.forgejo.org`, which does not carry that action.
### 2 · Three release-pipeline safety changes
- `detect` and the Renovate job get an explicit `repository_owner` guard. The directory split only holds while `.forgejo/` is non-empty; empty it and Codeberg would fall back to `.gitea/` and start running these on the contributor-facing runner, without secrets.
- `detect` now reads tags from **Codeberg**, not from the Gitea instance it runs on. Push mirroring is `git push --mirror`, so a tag minted on Gitea is deleted by the next sync until the Codeberg tag push propagates back — asking Gitea inside that window reports "no tag" for an already-shipped release and would cut it twice. It also now fails on any status other than 200/404 rather than reading a transient error as "no tag": a failed job is recoverable, a duplicate release is not.
- **The Codeberg publish step pushes the tag itself** instead of waiting for it to arrive by mirror. That wait was correct while Gitea mirrored *to* Codeberg; under Codeberg-canonical the mirror runs the other way and it would never resolve. Attaching the release to an already-pushed ref (no `target_commitish`) is what avoids the empty-bodied 500s, and the create call retries with backoff because Codeberg 500s on a tag it has only just received. The step stays **fail-loud**, not `continue-on-error` — it reported green through 0.2.1–0.3.2 while never once publishing, and that must not be possible again.
### 3 · Renovate
`renovate.json5` plus a Gitea-side job targeting Codeberg's API. `managerFilePatterns` covers **both** workflow directories, so the pinned Renovate image tag and the action versions in either file keep getting bumped.
Needs two new Gitea secrets: `RENOVATE_TOKEN` (Codeberg bot, repo read/write + PR scope) and `GITHUB_COM_TOKEN` (read-only github.com PAT, for changelog lookups).
### 4 · Weblate
A parity check (`scripts/check_translations.py`) runs on every PR without a path filter, so the required `Translations` status is always reported. Partial translations are expected, so `MissingTranslation` and `MissingQuantity` become informational — `ExtraTranslation` stays fatal. Agendula had no `lint` block at all, so the first locale to land would otherwise have failed the build.
**Settings → App language** now opens a picker carrying a "Help translate" header. That is why it drops floret-kit's `LanguagePickerRow` for a local row: the shared recipe has no `header` slot, and the framing is app-specific rather than a family primitive.
### 5 · Links repointed
In-app Source / License / report-issue URLs, F-Droid metadata, README (now with a Codeberg CI badge), and the docs.
`floret-kit` follows suit — `.gitmodules` points at `codeberg.org/jlmakiola/floret-kit`, so a clone no longer needs to reach the personal Gitea instance to resolve it. The Gitea copy is **kept**: every existing tag records the old submodule URL, so rebuilds of past releases still resolve.
### 6 · Housekeeping
Drops `release-notes.md` — a release-pipeline scratch file that got committed — and gitignores the five others the release job writes into the workspace.
### Not in this PR
The Codeberg → Gitea push mirror, the Weblate component, and the Codeberg bot account (all browser-side). Until the mirror is flipped, merging this does **not** reach the Gitea runner.
Co-authored-by: Jean-Luc Makiola <business@jeanlucmakiola.de>
Reviewed-on: https://codeberg.org/jlmakiola/agendula/pulls/2
Add a Material 3 SearchBar overlaying the top of the overview. Collapsed it
is a "Search tasks" bar below the title row; tapping expands it in place to
cover the home content with live results, filtering every task (open and
completed) by title, case-insensitive. The leading icon flips to a back
arrow while expanded, a clear button empties the query, and the FAB hides so
it does not float over the results. Results reuse the upcoming preview row.
ListsViewModel.Content now carries allTasks (open + completed) as the search
corpus; filtering stays in memory so the provider query is untouched.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rework the overview's 2x2 smart grid into a daily-momentum layout:
- Promote Today into a full-width hero with an M3 Expressive
CircularWavyProgressIndicator over "x of y done" for tasks due today.
Empty/all-done states read as a calm finished state, not a bare 0.
- Drop the white "Upcoming 0" tile (it shouted loudest while carrying the
least) in favour of a live preview of the next few upcoming tasks, each a
slim row with the quiet meta line and a tap-through to the task.
- Overdue + All fall back to a 2-up of the existing tonal tiles.
ListsViewModel now combines a Smart(COMPLETED) flow so the ring can count
the tasks already ticked off today (the open smart lists drop them).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Removes the doubled add idiom (top inline-add bar + FAB). A new "Bottom quick-add bar" setting (default off) picks one affordance per screen: off keeps the floating New task button everywhere; on shows a quick-add field pinned to the bottom of a real list (smart lists keep the button, as they have no single target list).
The bottom field floats on the brightest container tone with a shadow so it stands off the task cards, rides above the keyboard (ime ∪ navigation-bar insets), and drops focus when the IME hides so no stray cursor lingers.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds crash reporting to Agendula by drawing the kit's new core-crash module
(first Android-library module in floret-kit), and finishes the core-time wiring
that a Phase 0 staging slip left uncommitted on main.
core-crash (NEW capability — Agendula had none):
- CrashReporter.install() in AgendulaApp captures uncaught exceptions on-device
(allowlist-only report, chains to the platform handler, uploads nothing).
- MainActivity routes to a standalone CrashReportActivity on a startup crash-loop,
and surfaces a single captured crash as a dialog on next launch; markHealthy on
resume. App label + issue-tracker URL flow in via CrashConfig.
- Thin app-side CrashReportActivity uses AgendulaTheme; the reusable machinery
(reporter, dialog, submit, config) lives in the kit. Submodule re-pinned to the
core-crash kit commit.
Completes Phase 0 (was only partially committed in ec7b696):
- Repoint DayWindow + Instant formatting imports to de.jeanlucmakiola.floret.time
(the app copies were deleted in ec7b696 but the imports/includeBuild/dependency
were never staged, leaving main non-building).
- settings.gradle.kts includeBuild("floret-kit"); app depends on core-time.
Also fixes the CI checkout YAML: the submodules block was mis-indented under
'uses:' (invalid step mapping) — dedented to a proper sibling.
Clean composite build + unit tests + lintDebug + debug assemble green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Floret is promoted to the family / design-language (shared-kit) name; the
tasks app itself becomes Agendula (de.jeanlucmakiola.agendula) — agenda
('things to be done') + Calendula's -ula, a twin of the Calendula name.
Renames the package, namespace, applicationId, rootProject.name, app_name,
FloretApp/FloretNavHost/FloretTransitions classes, theme, F-Droid metadata
dir, CI artifact name, and docs. The botanical word 'florets' is preserved in
the name-origin prose, which is rewritten to Agendula's etymology. Clean
build + unit tests green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reminders onboarding & polish, plus the Settings screen (landed early).
- One-time reminder onboarding gate after the provider grant
(ReminderOnboardingScreen + OnboardingScaffold), requesting
POST_NOTIFICATIONS on API 33+; choice recorded in prefs.
- Settings screen, structured after Calendula as a category hub with
sliding sub-screens (CollapsingScaffold + grouped rows + full-screen
OptionPicker): About card, Appearance (theme, dynamic colour), Task
form (default edit-form fields, default list, add-a-subtask-row
opt-out), Reminders, Language, and Report a problem. Reached via an
expressive shaped action button (MaterialShapes.Cookie4Sided) on the
lists overview. Back from a sub-screen returns to the hub.
- Reminders: master enable toggle (gates the whole engine; re-requests
the notification permission), a default lead time with a Custom
amount/unit editor, an Android-12 exact-alarm status row, and
per-list overrides of the default (Inherit / None / custom lead),
honoured by ReminderScheduler per task.
- Per-app language (AppLanguage + locales_config.xml + appcompat
locales service); add-a-subtask-row opt-out wired into the task list.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
M3 detail/edit polish:
- "Progress" slider on the edit form writes Tasks.PERCENT_COMPLETE (clamped
0-100, 5% detents); status stays owned by the complete toggle.
- Conflict-safe saves: updateTask re-checks the provider's last_modified against
the value captured when the form loaded and throws TaskConflictException; the
editor offers overwrite-or-cancel instead of clobbering an external change.
M4 subtasks (UI):
- Reparent: a "Parent task" picker files a task under any top-level task in its
list (or "None" to promote it); candidates stay top-level to keep nesting one
level deep. Switching list clears the now-invalid parent.
- Tapping a subtask opens its own detail (a new TaskDetail entry, own VM).
Tests: percent clamping, parent-id write, populatedFields reveal logic.
Docs: ROADMAP M2/M3/M4 reconciled to the codebase; CHANGELOG [Unreleased].
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the boxed-text-field form with the family's tonal-card layout:
Close + filled SAVE top bar, a borderless headline title with a
list-colour accent bar, a "When" card (all-day switch + tappable
start/due rows), a tappable list card and reminder card that open
OptionCard dialogs, and segmented-button priority.
Optional Description / Priority / Reminder sections unfold from a
"More fields" picker (Calendula's disclosure): TaskFormField enum +
populatedFields() auto-reveal fields that already carry a value, and a
persisted defaultEditFields pref decides which start open (the settings
UI to edit it lands with M6). New shared InlineTextField + OptionCard;
date/time conversion helpers in DateTimeField promoted to internal.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the base scaffolds with the real Material 3 Expressive UI on the
existing M1 ViewModels.
Task list: due-date section headers (collapsible Completed), grouped tonal
rows with a list-colour accent bar, swipe-to-complete / -delete, inline add,
list-name chip and subtask "done/total" chip in mixed views (children folded
into their parent). New pure TaskSections grouping + unit test.
Detail: each fact its own tonal card with a leading icon (Calendula pattern) —
when/list/priority/progress/description; progress shows only with subtasks.
Subtasks are a connected grouped run whose first segment is the expand toggle,
with an accent-tinted inline add row; completed subtasks sink to the bottom.
Edit: title, description, list dropdown, start/due date-time + all-day,
priority segmented buttons, reminder offset, validation; IME auto-scroll.
Extract GroupedSurface from GroupedRow so the connected-row look is reusable
at any width; add ListNameChip + date-time format/picker helpers.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the RootScreen nav stubs with a real NavHost. Each destination
binds its existing M1 ViewModel from route args.
- ui/navigation: Dest route table (string-based; no serialization plugin)
+ FloretNavHost (lists -> task list -> detail / edit).
- Base scaffolds for TaskListScreen, TaskDetailScreen, TaskEditScreen,
wired to their ViewModels — skeletons for the rich M2/M3 UI.
- RootScreen READY branch now hands off to FloretNavHost.
- Add navigation-compose 2.9.0; supporting strings.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
First real screen replacing the scaffold: LargeTopAppBar, smart-list cards
in a 2x2 grid with correct tonal pairings (Today=primary, Overdue=error,
Upcoming=tertiary, All=secondary containers), user lists grouped by account
with colour dots + open counts, and an Extended FAB. Consumes the live
ListsViewModel; navigation callbacks stubbed for the next screens.
Verified on device against real tasks.org/DAVx5 data.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Non-visual stack (backoffice) complete and verified against the real
provider semantics (tasks.org's bundled dmfs TaskProvider, authority
org.tasks.opentasks, org.tasks.permission.* dangerous):
- TaskContract subset + ProviderResolver (runtime authority/permission)
- AndroidTasksDataSource (Instances query, ContentObserver) + TasksRepository
with live Flows; domain models, smart-list filtering, sorting, form validation
- Self-scheduled due-reminder engine (AlarmManager, boot/provider-change)
- Render-only ViewModels + UiState for every screen
- 24 JVM unit tests; assembleDebug + lintDebug + testDebugUnitTest green
- RootScreen is a functional scaffold over real data, to be replaced with the
Material 3 Expressive screens one by one
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sibling to Calendula. Pure front-end posture; TaskContract data layer,
task screens and reminder engine to follow (see docs/PLAN.md).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>