Commit Graph

78 Commits

Author SHA1 Message Date
f978c3727c feat(provider): ship our own task store, vendored under our own authority
Steps 2 and 3 of docs/STORAGE-AND-SYNC.md. Agendula stops depending on a tasks
provider app being installed: it now carries one.

The module

New :provider — the dmfs task provider 1.4.2 (Apache-2.0, DB 23), vendored
in-tree, renamed to authority de.jeanlucmakiola.agendula.tasks and permissions
de.jeanlucmakiola.agendula.permission.*. It coexists with OpenTasks and
tasks.org rather than replacing them; nothing collides with org.dmfs.*, so both
can be installed at once. The contract shape is untouched — same tables, same
columns — because that is what our data layer and every CalDAV engine already
speak. We own the namespace it lives in, not the schema.

Vendored rather than depended on because the permission names are hardcoded in
the upstream AAR's manifest and cannot be renamed in a prebuilt artifact; in-tree
also satisfies F-Droid's from-source rule. provider/PROVENANCE.md records the
upstream commit and every deviation, each marked with an AGENDULA CHANGE comment
at the site so the list and the code cannot drift apart.

The change that matters most is the account cleanup. Upstream holds GET_ACCOUNTS
and deletes any task list whose account it cannot see. We dropped that permission
— we only ever need our own accounts, which are visible without it — but an
account we cannot see is indistinguishable from one that was removed, so left
alone the provider would quietly delete synced lists. Cleanup is now restricted
to account types this package authenticates itself, which is currently none.
ProviderAccountCleanupTest pins that, and answers open question 3: the local path
works with no account present at all.

Also required by targetSdk 36, none of which upstream faced at 29:
FLAG_IMMUTABLE on the notification PendingIntent, an inexact-alarm fallback so a
revoked SCHEDULE_EXACT_ALARM cannot kill the app on a timezone change, and an
explicit android:exported on the receiver.

Storage modes

ProviderResolver gains a StorageMode: LOCAL (our provider) or EXTERNAL (an
installed one). Not a third SYNCED value — synced is LOCAL with an account
attached, which is derived state, and modelling it as a separate store would
imply switching sync on is a migration. It isn't.

When the user has not chosen, the tell is whether we already hold an external
provider's runtime permission. That permission is dangerous-level, so it can only
be there because an earlier version asked and they agreed — the signature of an
existing Posture A user, who must not be dropped onto an empty store. Fresh
installs get local-first.

hasPermission now short-circuits for our own provider: same-uid access bypasses
the check outright, so ProviderStatus.NEEDS_PERMISSION can no longer fire in
Local mode. That was the work item the storage-and-sync doc called for. The
resolver's platform calls moved behind ProviderEnvironment so the decision — the
part that loses people their data if wrong — is unit-tested on the JVM.

Verified: 51 vendored provider tests pass, app tests pass, lintDebug and
assembleDebug clean. ProviderAccountCleanupTest skips on ARM64, where Robolectric
has no SQLite backend, and runs on x86_64 CI. Not yet exercised on a device.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 21:19:52 +02:00
3d54a896ce Merge branch 'fix/provider-interaction-review'
Corrects the tasks-provider interaction end to end (see 47cf99a). Merged as
step 1 of the storage-and-sync sequence in docs/STORAGE-AND-SYNC.md: it touches
the same permission flow that the :provider vendoring is about to change, so it
lands first.

The floret-kit submodule pointer keeps main's e047a2b, which already contains
the branch's 396e538.

# Conflicts:
#	app/src/main/java/de/jeanlucmakiola/agendula/ui/detail/TaskDetailScreen.kt
2026-08-02 20:53:55 +02:00
Jean-Luc Makiola
c47653c9cb Merge pull request 'Use floret-kit's shared snack chip' (#6) from feat/shared-snack-chip into main
Some checks failed
Release — F-Droid repo + Gitea/Codeberg release / detect (push) Successful in 7s
Release — F-Droid repo + Gitea/Codeberg release / release (push) Has been skipped
Renovate / renovate (push) Failing after 43s
2026-08-02 20:46:11 +02:00
e639e250b7 Use floret-kit's shared snack chip
The undo chip moves into the kit so Calendula's reschedule confirmation is
the same component. Re-pins the kit onto the branch that carries it.
2026-08-02 20:45:39 +02:00
Jean-Luc Makiola
58a50512bf docs: settle the storage and sync direction (#5)
Reviewed-on: https://codeberg.org/jlmakiola/agendula/pulls/5
2026-08-02 17:58:13 +02:00
Jean-Luc Makiola
1add1fcadb chore: re-pin floret-kit and pass allDay at the date call sites (#4)
Reviewed-on: https://codeberg.org/jlmakiola/agendula/pulls/4
2026-08-02 17:57:58 +02:00
Jean-Luc Makiola
217d5d7afd Move the canonical forge to Codeberg, and add Renovate + Weblate (#2)
Some checks failed
Release — F-Droid repo + Gitea/Codeberg release / release (push) Has been skipped
Release — F-Droid repo + Gitea/Codeberg release / detect (push) Successful in 7s
Renovate / renovate (push) Failing after 43s
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
2026-08-01 16:34:20 +02:00
47cf99af32 fix(data): correct the tasks-provider interaction end to end
A review of every path through the OpenTasks/tasks.org ContentProvider,
prompted by edited due times reverting. Four independent defects produced that
one symptom, plus several unrelated ones alongside.

Edits reverting
- The edit form was bound from a LaunchedEffect in the nav host while its
  ViewModel survives on the back stack, and bindEdit replaced state wholesale.
  MainActivity declares no configChanges, so any Activity recreation (rotation,
  theme/font/display-size change, split-screen, unfolding) re-fired the effect
  and overwrote in-progress edits with the stored row. Guarded with a `bound`
  flag; picker state moved to rememberSaveable so an open picker also survives.

All-day handling
- All-day items are date-only in iCalendar and belong at UTC midnight with a
  null tz. The app wrote *local* midnight, so in Berlin an all-day task drifted
  back a day on every save cycle, corrupting anything synced. Rendering had the
  mirror bug, so the two cancelled out locally and hid each other.
- Toggling the all-day switch flipped the flag but left the timestamp, so an
  all-day task toggled off read back as 02:00 — another apparent "time reset".
- New domain/AllDayTime.kt owns the two conventions and the conversion between
  them; the picker, the write mapper and the toggle all go through it.

Provider write contract
- DUE and DURATION are mutually exclusive and the provider validates the merged
  row, so saving a due date onto a task that carried a duration threw
  IllegalArgumentException — the save simply failed. DURATION is now cleared
  alongside every time write.
- A recurring task's start/due are read from the instances view, and writing
  them back to tasks/<id> re-anchored the whole series. Updates now go through
  instances/<id>, where the provider forks an override instead.
- Recurrence is derived from rrule/rdate rather than the is_recurring column:
  that column only exists from OpenTasks 1.4.0 (DB 23) and is absent on
  tasks.org's bundled provider (DB 22), where it would report every recurring
  task as one-off and send its edits to the anchor.

Reminders
- The per-task Reminder field in the edit form was inert: never persisted,
  never read back, and REMINDER_WITHOUT_DUE could block a save over a value
  that was discarded regardless. Leads are now stored as Alarm property rows
  and preferred over the per-list/global setting. Written before the task
  update so a recurrence fork copies them onto the override. Note the provider
  fires nothing itself — ReminderScheduler still arms the alarm.
- Reminders were keyed by task id over rows read from the instances view, so
  .toMap() collapsed a recurring task to one arbitrary occurrence (the query is
  unsorted). Now keyed per occurrence, with request codes and intent data to
  match. Missed reminders within 6h fire once on boot instead of being dropped.

Robustness
- Four terminal `catch`es killed their upstream on the first provider failure.
  SettingsViewModel is collected in setContent above the permission gate for the
  Activity's lifetime, so a pre-grant SecurityException left the list picker
  empty until the process restarted. Replaced with capped-backoff retry.
- lazyChildren had no catch at all; an exception escaped stateIn past
  viewModelScope's SupervisorJob and crashed the process.
- Observer registration is all-or-nothing (the second register throwing leaked
  the first), ProviderChangeReceiver validates action and authority and
  debounces, and the permission gate re-checks on resume.

Also drops the unused DateTimeField composable and the stale INSTANCES
projection, which omitted the recurrence columns the mapper now depends on.

Bumps floret-kit to pick up the matching all-day formatting fix.

Verified by unit tests (43 app, 15 core-time) and a clean assembleDebug; the
provider interaction itself has not been exercised on a device.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 22:13:26 +02:00
26628dc0bb chore: drop stray Codeberg release scratch files
All checks were successful
Release — F-Droid repo + Gitea/Codeberg release / detect (push) Successful in 7s
Release — F-Droid repo + Gitea/Codeberg release / release (push) Has been skipped
cb-payload.json and cb-response.json are generated at runtime by the
release workflow; they were committed by accident from a local run and
still held stale v0.3.1 data.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 21:30:13 +02:00
d2e3832ef2 Merge branch 'chore/floret-kit-multi-reminders' 2026-07-20 21:04:05 +02:00
93857135b3 Merge branch 'home-today-ring-upcoming' 2026-07-20 21:04:02 +02:00
36beb2d0ad ci(release): wait for the mirrored tag, never mint it
The Codeberg publish has never once succeeded — 0.2.1, 0.2.2, 0.3.0, 0.3.1
and 0.3.2 all failed, and 0.3.0 was published by hand. Neither previous fix
could have worked, because the cause isn't in this file: ref writes to
jlmakiola/agendula on Codeberg fail. A tag push returns "cannot lock
references" and POST /tags returns an empty-bodied 500, while the identical
calls succeed on jlmakiola/calendula with the same token. Creating a release
mints a tag, so it inherited the same failure.

Attaching a release to a tag that is ALREADY there needs no ref write and
returns 201. So split the responsibility the way it should have been: the
push mirror owns delivering the tag, this step only attaches to it. Poll for
the tag, then POST with no target_commitish so the API attaches rather than
resolves a commit and mints one. If the tag never lands, fail with a pointer
at the mirror instead of trying to create it.

Drop continue-on-error. It reported green across five releases that never
published, which is how 0.3.1's crash fix reached F-Droid while the Codeberg
and Obtainium users who hit the crash got nothing.

Tested against the live API before committing (unlike its predecessors):
tag-present attaches 201, tag-absent exits 1 with the diagnostic. Note this
does not by itself restore publishing — the mirror cannot write the v0.3.2
tag either, so Codeberg must repair the repo's ref store first.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 19:50:57 +02:00
bc70ed3a9f release: cut 0.3.2 — get the crash fix onto Codeberg
All checks were successful
Release — F-Droid repo + Gitea/Codeberg release / detect (push) Successful in 7s
Release — F-Droid repo + Gitea/Codeberg release / release (push) Successful in 11m59s
v0.3.2
2026-07-20 19:11:55 +02:00
3f166ef5f0 release: cut 0.3.2 — get the crash fix onto Codeberg
No app-code change from 0.3.1: this exists to re-run the release pipeline
now that the Codeberg publish step is fixed. 0.3.1's APK reached F-Droid but
its Codeberg release 500'd, so anyone installing from Codeberg or Obtainium
is still on the crashing 0.3.0 — cutting 0.3.2 gets them the fix and proves
the workflow fix in the only way that counts, a real release.

Chosen over hand-patching the 0.3.1 release onto Codeberg: same outcome for
users, minus a manual APK upload, and it actually exercises the pipeline.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 19:11:48 +02:00
41bd49826a ci(release): create the Codeberg release while the tag is absent
All checks were successful
Release — F-Droid repo + Gitea/Codeberg release / detect (push) Successful in 6s
Release — F-Droid repo + Gitea/Codeberg release / release (push) Has been skipped
2026-07-20 19:10:56 +02:00
05c75bafa7 ci(release): create the Codeberg release while the tag is absent
Revert 4aa65ed's approach. It read the Forgejo 500 as being caused by
target_commitish, and pushed the tag to Codeberg first so the release could
attach to an existing tag without one. That inverted the actual failure:
Forgejo 500s on POST /releases precisely WHEN the tag already exists, so
pre-pushing it guarantees the error it was meant to avoid.

The second half then can't recover — a bare tag is not a release, so
GET /releases/tags 404s and the upsert has no id, which is the
"Could not resolve Codeberg release id" that ended the 0.3.1 run.

0.3.0 published fine because it POSTed while the tag was still unsynced and
let the API mint tag + release together. Do that deliberately instead of by
luck: if no release owns the tag, delete the mirrored tag, then POST with
target_commitish. Branches mirror reliably (main was already at the release
commit when 0.3.1 failed), so the commit is present and only the raced-in
tag was in the way. Deleting it is safe exactly because no release owns it.

An existing release still takes the PATCH path and skips the delete, so
re-runs never disturb something already published.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 19:06:36 +02:00
cfa25b9730 release: cut 0.3.1 — fix the launch crash in minified builds (#1)
All checks were successful
Release — F-Droid repo + Gitea/Codeberg release / detect (push) Successful in 7s
Release — F-Droid repo + Gitea/Codeberg release / release (push) Successful in 11m31s
v0.3.1
2026-07-20 18:36:47 +02:00
9d7fc64b0b release: cut 0.3.1 — fix the launch crash in minified builds
R8 kept androidx.work.impl.WorkDatabase_Impl but pruned its no-arg
constructor: nothing calls it directly, Room only reaches it reflectively.
Room then threw InstantiationException, surfacing as "Failed to create an
instance of androidx.work.impl.WorkDatabase". WorkManager builds that
database from a startup ContentProvider, so 0.3.0 died before any of our
code ran — every install, every launch.

We don't depend on WorkManager directly; it arrives via Glance. AGP 9's
stricter R8 is what tipped this over, which is why 0.3.0 was the first
release to hit it.

Keep the Room no-arg constructor, and the ListenableWorker constructor
alongside it — same pruning hazard on the path WorkManager uses to
instantiate workers by name, which would have bitten once a Glance widget
update actually ran.

Reproduced the reporter's stack trace frame-for-frame on a releaseTest
build, then confirmed it launches clean afterwards. Fixes #1.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 18:35:41 +02:00
4aa65edb45 ci(release): push tag to Codeberg before creating the release
All checks were successful
Release — F-Droid repo + Gitea/Codeberg release / detect (push) Successful in 6s
Release — F-Droid repo + Gitea/Codeberg release / release (push) Has been skipped
The first fix didn't help: the pipeline creates the tag via the Gitea
API, and the push mirror (sync_on_commit only fires on real git pushes)
doesn't propagate an API-created tag promptly. So the Codeberg release
POST still raced the mirror and 500'd on a commit/tag Codeberg hadn't
received (0.2.1 and 0.3.0 both shipped everywhere but Codeberg).

Push the tag straight to Codeberg from the runner (guaranteed present),
then attach the release to that existing tag with no target_commitish —
which is what Forgejo 500s on. Race-free.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 22:48:29 +02:00
a9843e25b7 release: cut 0.3.0
All checks were successful
Release — F-Droid repo + Gitea/Codeberg release / detect (push) Successful in 7s
Release — F-Droid repo + Gitea/Codeberg release / release (push) Successful in 12m27s
Ships the M5 reminders onboarding + Settings screen alongside the reworked
overview (Today progress ring, live Upcoming preview, unfurling search) and
the real launcher mark.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
v0.3.0
2026-07-19 22:25:14 +02:00
5f4711dabe ui: move the action press flourish onto the shape, spin the gear
Some checks failed
Release — F-Droid repo + Gitea/Codeberg release / detect (push) Successful in 7s
Release — F-Droid repo + Gitea/Codeberg release / release (push) Has been cancelled
The press animation turned the glyph; put it on the scalloped cookie
container instead and make it stronger (scale 1→0.82, rotate 0→40°). The
glyph holds upright via a counter-rotation, so the shape spins while a
magnifier or list icon stays readable. New spinIcon opts a glyph into its own
quarter turn — the settings gear uses it, so it reads as a gear cranking.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 22:21:29 +02:00
6d3fbc05c1 home: unfurl search from a fixed search icon, drop the title
Rework the top bar into a custom row with no app title (the launcher icon
already names the app). Settings stays pinned at the right and the search
action sits just left of it; neither moves. Tapping search unfurls a pill
leftward from the magnifier (expandHorizontally anchored at the end) holding
the auto-focused query field and a clear button, with the search icon
remaining as the bar's fixed trailing icon. Results render over the home
content as you type; a blank query leaves the home screen visible. Tapping
the icon again or system back closes search; the FAB hides while searching.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 22:21:29 +02:00
721b411579 icon: centre the launcher mark on the canvas
The first pass shifted the mark off-centre. Scale 0.66 about the canvas
centre, and centre the task CARD (not the card+bloom bounding box, which the
overhanging bloom badge drags low): pivot the Y-scale at the card's centre
(y=242.76) and translate +13.24 so the card sits dead-centre, horizontally
and vertically, with the bloom badging out to the lower-right.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 22:21:29 +02:00
046b8f7e9e home: move search behind a top-bar action, not an always-on bar
The inline search bar was permanently visible. Replace it with a search
action button (a 6-sided cookie shape, sibling to the settings cookie) to
the left of settings: the search bar is absent until tapped, then opens
expanded and auto-focused, covering the home content with live results.
Back arrow or system back closes it; the FAB hides only while searching.
Pass windowInsets = 0 so the bar, already below the app bar, does not
re-apply the status-bar inset and float with a large top gap.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 22:21:29 +02:00
06cc9b1c8b icon: real launcher mark — task card, check, calendula bloom
Replace the placeholder check-mark foreground with the finished agendula
mark (converted from design/icon/agendula_icon.svg): a rounded line-art task
card with a check, plus a small Calendula bloom badge in the open
bottom-right corner — the sibling of Calendula's calendar mark.

Strokes render in Calendula's off-white (#FAF6F0) over agendula's existing
plum background (#7A5C6B, the hue-rotated counterpart of Calendula's slate),
so the two apps read as a family while staying distinct. Scaled 0.66 to
match Calendula's footprint and ~2.8dp stroke weight; reused as the
<monochrome> slot for themed icons.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 22:21:29 +02:00
2d366a7be3 home: inline expanding search over all tasks
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>
2026-07-19 22:21:29 +02:00
52aeebcb53 home: Today progress ring + live Upcoming preview
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>
2026-07-19 22:21:29 +02:00
8f6b85008c release: cut 0.2.2
Some checks failed
Release — F-Droid repo + Gitea/Codeberg release / detect (push) Successful in 6s
Release — F-Droid repo + Gitea/Codeberg release / release (push) Has been cancelled
Re-cut after 0.2.1's Codeberg publish failed (500 on pre-synced tag,
fixed in the previous commit). Validates the fix end to end and ships
the Codeberg direct-download channel (signed APK + SHA-256).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 22:14:57 +02:00
eb1e530ce7 ci(release): fix Codeberg publish 500 on pre-synced tag
All checks were successful
Release — F-Droid repo + Gitea/Codeberg release / detect (push) Successful in 6s
Release — F-Droid repo + Gitea/Codeberg release / release (push) Has been skipped
The push mirror (sync_on_commit) syncs the tag to Codeberg before the
publish step runs, and Forgejo returns HTTP 500 on POST /releases with a
target_commitish when the tag already exists — so the release was never
created (v0.2.1 shipped to Gitea/F-Droid but not Codeberg). Only pass
target_commitish when the tag isn't mirrored yet; otherwise attach the
release to the existing tag.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 22:07:37 +02:00
7516972e9f release: cut 0.2.1
All checks were successful
Release — F-Droid repo + Gitea/Codeberg release / detect (push) Successful in 6s
Release — F-Droid repo + Gitea/Codeberg release / release (push) Successful in 12m13s
Publish releases to the Codeberg mirror (signed APK + SHA-256 checksum)
as a direct-download channel. Bumps versionName to 0.2.1; the pipeline
mints the v0.2.1 tag on merge to main.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
v0.2.1
2026-07-19 21:53:49 +02:00
7f58f81fe1 ci: adopt the modern calendula pipeline + Codeberg mirror
Port Calendula's current CI/release pipeline:

- ci.yaml: pull_request-triggered, change-scope classification
  (docs/metadata-only PRs skip the Android build but still report a
  green CI), and a reproducible-release invariant guard.
- release.yaml: the committed versionName is the source of truth — a
  bump reaching main triggers the release, which builds, signs,
  publishes to the F-Droid repo, then mints the vX.Y.Z tag + Gitea
  release and mirrors it to Codeberg with the signed APK + SHA-256
  checksum. workflow_dispatch runs the re-sign-only recovery path.
- Gitea releases are flagged as pre-releases while MAJOR is 0.
- build.gradle.kts: reproducible-release invariants (vcsInfo,
  dependenciesInfo) + a releaseTest variant for the on-device gate.
- fastlane/ becomes the single source of truth for store metadata;
  the localized F-Droid layout is generated from it at release time.
- Port scripts/, .gitea/ISSUE_TEMPLATE/, and rewrite docs/RELEASING.md
  for the versionName-in-main model; fix stale references elsewhere.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 21:53:13 +02:00
976d496d21 reminders: adopt floret-kit multi-value override model
Bumps the floret-kit submodule to the multi-value core-reminders API
(ReminderOverride.Minutes now a List<Int>) and adapts the per-list override
plumbing + pickers to it. Agendula stays single-reminder: overrides are
one-element (or empty) lists, reminderLeadFor takes firstOrNull, and the
single-select picker wraps/unwraps listOf. On-disk format is unchanged (single
values round-trip byte-identically). Verified: :app:compileDebugKotlin.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 21:47:50 +02:00
245f1db536 ui: move the action press flourish onto the shape, spin the gear
The press animation turned the glyph; put it on the scalloped cookie
container instead and make it stronger (scale 1→0.82, rotate 0→40°). The
glyph holds upright via a counter-rotation, so the shape spins while a
magnifier or list icon stays readable. New spinIcon opts a glyph into its own
quarter turn — the settings gear uses it, so it reads as a gear cranking.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 00:01:07 +02:00
623e533547 home: unfurl search from a fixed search icon, drop the title
Rework the top bar into a custom row with no app title (the launcher icon
already names the app). Settings stays pinned at the right and the search
action sits just left of it; neither moves. Tapping search unfurls a pill
leftward from the magnifier (expandHorizontally anchored at the end) holding
the auto-focused query field and a clear button, with the search icon
remaining as the bar's fixed trailing icon. Results render over the home
content as you type; a blank query leaves the home screen visible. Tapping
the icon again or system back closes search; the FAB hides while searching.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 00:00:42 +02:00
2e50356f81 icon: centre the launcher mark on the canvas
The first pass shifted the mark off-centre. Scale 0.66 about the canvas
centre, and centre the task CARD (not the card+bloom bounding box, which the
overhanging bloom badge drags low): pivot the Y-scale at the card's centre
(y=242.76) and translate +13.24 so the card sits dead-centre, horizontally
and vertically, with the bloom badging out to the lower-right.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 23:35:03 +02:00
e6f503c02a home: move search behind a top-bar action, not an always-on bar
The inline search bar was permanently visible. Replace it with a search
action button (a 6-sided cookie shape, sibling to the settings cookie) to
the left of settings: the search bar is absent until tapped, then opens
expanded and auto-focused, covering the home content with live results.
Back arrow or system back closes it; the FAB hides only while searching.
Pass windowInsets = 0 so the bar, already below the app bar, does not
re-apply the status-bar inset and float with a large top gap.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 23:34:54 +02:00
c53511196d icon: real launcher mark — task card, check, calendula bloom
Replace the placeholder check-mark foreground with the finished agendula
mark (converted from design/icon/agendula_icon.svg): a rounded line-art task
card with a check, plus a small Calendula bloom badge in the open
bottom-right corner — the sibling of Calendula's calendar mark.

Strokes render in Calendula's off-white (#FAF6F0) over agendula's existing
plum background (#7A5C6B, the hue-rotated counterpart of Calendula's slate),
so the two apps read as a family while staying distinct. Scaled 0.66 to
match Calendula's footprint and ~2.8dp stroke weight; reused as the
<monochrome> slot for themed icons.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 23:07:25 +02:00
a8595e26b4 home: inline expanding search over all tasks
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>
2026-06-28 23:07:12 +02:00
411e27659f home: Today progress ring + live Upcoming preview
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>
2026-06-28 22:47:02 +02:00
b266653e4e chore: bump floret-kit submodule to latest (component superset)
All checks were successful
CI / ci (push) Successful in 4m0s
Adopt floret-kit main @ 566caf4, picking up the grown GroupedRow / OptionPicker / CollapsingScaffold family superset. App compiles and unit tests pass against it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 22:13:21 +02:00
2f5012dc34 tasks: consolidate add affordance into a setting (FAB or bottom bar)
All checks were successful
CI / ci (push) Successful in 4m8s
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>
2026-06-28 22:01:32 +02:00
ccc07e86c7 tasks: quiet single-line row meta, lazy subtasks in smart lists, polish
- Collapse a task row's supporting info (priority, due, subtask progress,
  list name) into one low-emphasis meta line instead of stacked filled
  chips: priority is a tinted flag, the list name carries its list colour,
  and dates are compact (formatDateTimeCompact).
- Expand subtasks in smart lists too, by lazily fetching a parent's full
  child set on demand (TasksRepository.subtasks + the view model's live
  lazyChildren), while per-list views stay query-free.
- animateItem on the rows so (un)expanding a group fades its subtasks
  in/out and slides the rows below — clean whether the children were
  already loaded or arrive a frame later.
- Sit the undo chip beside the FAB at its height rather than overlapping
  or floating above it.

Bumps the floret-kit submodule for formatDateTimeCompact.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 20:47:50 +02:00
211450bc46 chore: bump floret-kit submodule to merged main
All checks were successful
CI / ci (push) Successful in 7m24s
Picks up the pushed floret-kit main (the eight shared-kit commits merged with
the upstream docs/changelog/license commit).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 19:20:56 +02:00
9d134be621 nav: fix blank back-gesture preview — let navigation drive predictive back
The per-screen Modifier.predictiveBack scaled the leaving screen with a
graphicsLayer, but NavHost only composes one back-stack entry at a time, so
nothing was drawn behind the shrinking screen — the gesture revealed the white
window background instead of the previous screen.

Drive predictive back through navigation-compose instead: it composes the
destination being revealed and seeks popExit with the gesture. popExit is now a
scaleOut + fade peek, so the leaving screen shrinks to reveal the real previous
screen held static. Drops the predictiveBack modifier from the destinations and
Settings (its inner section BackHandler is unchanged). The kit's predictiveBack
stays for overlay-style screens.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 19:15:24 +02:00
37e3c4e659 settings: use the shared AboutCard + LanguagePickerRow
Drop the in-app AboutCard, LanguageRow and languageLabel in favour of the
floret-kit recipes; the app still supplies its logo, strings, links and row
icon. Bumps the submodule.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 15:24:17 +02:00
78632865f9 edit: use the shared OptionalFormSection in the task form
Drop the in-app copy in favour of floret-kit's. Bumps the submodule.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 15:17:57 +02:00
e7a62e71df reminders: use the shared DialogAmountField in the custom lead editor
Replace the inline tonal amount field in the reminder picker with floret-kit's
DialogAmountField. Bumps the submodule.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 15:15:08 +02:00
4c05c86e95 motion: use floret-kit's expandEnter/collapseExit for revealable sections
Replace the inline expandVertically/shrinkVertically + fade in the reminder
picker, task edit form and settings with the shared transitions — which adds
reduced-motion support and the Expressive springs. Bumps the submodule.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 15:13:06 +02:00
37662e83bb nav: swap the full slide-back for the predictive-back peek
Back was a full-width horizontal slide-off — too heavy. Replace it with
floret-kit's Modifier.predictiveBack on each pop-able destination (task list,
detail, edit) so the leaving screen scales/rounds with the gesture and the held
screen is revealed unmoved; NavHost now does no pop animation. Settings applies
it at the hub only (enabled = section == null) so its inner section back still
returns to the hub. Forward stays a calm fade-in.

Bumps the floret-kit submodule.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 14:31:20 +02:00
63c6191676 onboarding: draw the onboarding scaffold from floret-kit
Use components' OnboardingScaffold / BenefitRow / OnboardingSpace and drop the
in-app copies; keep Agendula's own SquircleHero (now in OnboardingHero.kt).
Bumps the floret-kit submodule.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 14:16:56 +02:00