Release v2.22.0 (#355)
Release — F-Droid repo + Gitea/Codeberg release + Play / detect (push) Successful in 8s
Release — F-Droid repo + Gitea/Codeberg release + Play / release (push) Successful in 14m16s
Release — F-Droid repo + Gitea/Codeberg release + Play / play (push) Failing after 1m26s

Views: navigation, transitions and split-view behaviour.

See CHANGELOG.md for the full list.

Co-authored-by: Jean-Luc Makiola <business@jeanlucmakiola.de>
Reviewed-on: https://codeberg.org/jlmakiola/calendula/pulls/355
This commit is contained in:
Jean-Luc Makiola
2026-10-01 16:21:11 +02:00
co-authored by makiolaj
parent bfc05795f0
commit 5f07adef2f
228 changed files with 6158 additions and 3055 deletions
+35 -3
View File
@@ -27,7 +27,7 @@ the package list (recurring writes, save conflicts, reminder delivery).
```mermaid
flowchart TD
subgraph UI ["ui/ — Compose screens + ViewModels"]
Screens["Month / Week / Day\nDetail / Edit / Settings\nOnboarding wizard"]
Screens["Month / Timeline / Agenda\nDetail / Edit / Settings\nOnboarding wizard"]
end
subgraph Data ["data/"]
Repo["CalendarRepository\n(interface + impl, Flow-based, io-dispatched)"]
@@ -79,10 +79,42 @@ flowchart TD
There is no navigation library. `MainActivity` hosts `RootScreen`, which
gates on the first-launch wizard (`ui/onboarding/`), then shows
`CalendarHost`. `CalendarHost` holds the active view (month/week/day)
plus overlay state for detail, edit, and settings — full-screen overlays
`CalendarHost`. `CalendarHost` holds the active view (month, day/multi-day/week,
agenda) plus overlay state for detail, edit, and settings — full-screen overlays
driven by `AnimatedVisibility` with a *held-key* pattern: the last shown
key stays alive through the slide-out so content never flashes empty.
Overlays leave through floret-kit's `predictiveBackExit`, so a committed back
gesture finishes from its scaled preview.
### Switching views (#184)
- **One focused date.** `CalendarHost` owns a `ViewFocus` (`LocalViewFocus`)
that every view opens on and carries along. A view *reads* it once, on
entry, synchronously (`EnterOnFocus` before it reads its own position, so
its first frame is already on that date), and *writes* it only when the user
moves — a pager settling (`focusOnPage` keeps the date's place in the page),
a jump, Today, a tapped day, the agenda's top row. Writing on entry would
drift the date on every round trip.
- **Three screens, not five.** The host's `AnimatedContent` is keyed on
`ViewScreen` (month, timeline, agenda), not on the view: day, multi-day and
week are one `TimelineScreen` with a `TimelineKind` each and a view model
each. Keying the three on one `contentKey` instead would still replay the
enter transition on every switch among them.
- **Timeline ↔ timeline resizes in place.** `ColumnGeometry` places every
column (header cell, all-day bar, day column) in the layout pass; a switch
swaps the pager for a frame over both pages' days, each column lerped
between its slot on the one page and the other — a day the page doesn't
show carries on at its pitch, off screen, which is what slides it in or
out. Blocks sit in their column as shares of its width (`LaneColumn`), so
nothing recomposes per frame. The frame starts and ends exactly as the two
pages lay out, so the pagers either side hand over without a jump.
- **Across screens, what both show morphs.** Inside a `SharedTransitionLayout`,
an event shown in both views travels from its old bounds to its new ones,
keyed per occurrence *and* per day it is drawn from (`ViewMorphKey`); only a
pager's settled page carries the tags. The drawer, top bar and FAB are not
per screen: CalendarHost draws them once, above the switch, from what the
screen on show publishes (`PublishChrome` in `CalendarChrome.kt`), so a
switch only changes what they say.
A tapped reminder notification routes through `MainActivity` (`singleTop` +
`onNewIntent`) as an external detail key that `CalendarHost` consumes
exactly like an event tap.
+7 -7
View File
@@ -1,7 +1,7 @@
# Building from source
Calendula builds with the standard Android Gradle toolchain — no extra setup
beyond the SDK, a JDK, and the submodule.
Calendula builds with the standard Android Gradle toolchain. You need the SDK,
a JDK and the submodule, nothing else.
## Clone
@@ -9,9 +9,9 @@ beyond the SDK, a JDK, and the submodule.
git clone --recurse-submodules https://codeberg.org/jlmakiola/calendula.git
```
Calendula depends on **[floret-kit](https://codeberg.org/jlmakiola/floret-kit)**,
Calendula depends on [floret-kit](https://codeberg.org/jlmakiola/floret-kit),
the shared Material 3 Expressive kit, as a git submodule wired in as a Gradle
composite build (`includeBuild("floret-kit")` in `settings.gradle.kts`) — it is
composite build (`includeBuild("floret-kit")` in `settings.gradle.kts`). It is
compiled from source, not resolved from a repository. A clone without the
submodule fails to configure. For an existing clone:
@@ -21,14 +21,14 @@ git submodule update --init --recursive
## Requirements
- **JDK 17** — not newer; the Android Gradle Plugin requires exactly 17. If your
- **JDK 17**, not newer: the Android Gradle Plugin requires exactly 17. If your
default JDK differs, set `JAVA_HOME` explicitly.
- **Android SDK** — platform **37** (`compileSdk`) and **build-tools 36.0.0**.
- **Android SDK**: platform 37 (`compileSdk`) and build-tools 36.0.0.
`minSdk` is 29, `targetSdk` 36.
The SDK is located via `ANDROID_HOME` (or `ANDROID_SDK_ROOT`), or via a
gitignored `local.properties` with `sdk.dir`. If you use `local.properties`, note
that the composite build needs **its own** copy at `floret-kit/local.properties`;
that the composite build needs its own copy at `floret-kit/local.properties`;
setting `ANDROID_HOME` covers both builds at once and is the simpler route.
The Gradle wrapper is checked in, so you don't need a system Gradle.
+13 -13
View File
@@ -26,7 +26,7 @@ Applies to the Android app **Calendula** (package `de.jeanlucmakiola.calendula`)
## In short
Calendula collects nothing, sends nothing, and has no user accounts. It has **no internet permission at all** — the app is technically incapable of transmitting your data anywhere. Everything it shows you is read from the calendars that already exist on your device.
Calendula collects nothing, sends nothing, and has no user accounts. It has **no internet permission at all**, so it cannot transmit your data anywhere. Everything it shows you is read from the calendars that already exist on your device.
## 1. Controller
@@ -47,9 +47,9 @@ All of the following is processed **locally on your device only**. None of it is
### Calendar data
Calendula is a viewer and editor for the calendars Android already manages. It reads and writes events, reminders and calendar settings through Android's system calendar provider. The app keeps **no database of its own** — your events live in the system calendar, exactly where they lived before you installed Calendula, and they remain there if you uninstall it.
Calendula is a viewer and editor for the calendars Android already manages. It reads and writes events, reminders and calendar settings through Android's system calendar provider. The app keeps **no database of its own**. Your events live in the system calendar, where they were before you installed Calendula, and they stay there if you uninstall it.
Note: if one of those system calendars is itself synchronised with an online account (for example a Google account, or a CalDAV server via DAVx5), that synchronisation is performed by Android and that other app — not by Calendula. The privacy policy of the respective provider applies to it.
Note: if one of those system calendars is itself synchronised with an online account (for example a Google account, or a CalDAV server via DAVx5), that synchronisation is performed by Android and that other app, not by Calendula. The privacy policy of the respective provider applies to it.
### Contacts (optional)
@@ -67,7 +67,7 @@ When you import or export an ICS file, Calendula reads or writes exactly the fil
Your preferences (view options, theme, reminder defaults and similar) are stored locally on your device and are removed when you uninstall the app.
## 4. Crash reports — the only case where data can leave your device
## 4. Crash reports: the only case where data can leave your device
If Calendula crashes, it offers to report the problem. Nothing is sent automatically. The report is copied to your clipboard and your browser is opened with the project's issue tracker, the text pre-filled. **You see the full content, you decide whether to submit it, and you can edit or discard it.**
@@ -86,19 +86,19 @@ If you choose to submit it, the report becomes a public issue on the project's i
## 5. External links
The settings screen contains links to the source code, the licence, the issue tracker and a voluntary donation page (Ko-fi). Following one of these links opens your browser and leaves the app; the privacy policy of the respective website then applies. Calendula transmits no data of yours in the process — it only opens the address.
The settings screen contains links to the source code, the licence, the issue tracker and a voluntary donation page (Ko-fi). Following one of these links opens your browser and leaves the app; the privacy policy of the respective website then applies. Calendula transmits none of your data in the process; it only opens the address.
## 6. Permissions and why they exist
- `READ_CALENDAR`, `WRITE_CALENDAR` — display and edit your events; the core function.
- `POST_NOTIFICATIONS` — show reminders.
- `READ_CONTACTS` — optional, only for the “Contact special dates” feature.
- `USE_EXACT_ALARM`, `SCHEDULE_EXACT_ALARM` — deliver reminders at the exact time, including after snoozing.
- `RECEIVE_BOOT_COMPLETED` — re-register pending reminders after a restart.
- `REQUEST_IGNORE_BATTERY_OPTIMIZATIONS` — only to open the system dialog for the “Reliable delivery” setting.
- `WAKE_LOCK`, `FOREGROUND_SERVICE`, `ACCESS_NETWORK_STATE` — required by the Android system component used for scheduled background work (WorkManager).
- `READ_CALENDAR`, `WRITE_CALENDAR`: display and edit your events (the core function).
- `POST_NOTIFICATIONS`: show reminders.
- `READ_CONTACTS`: optional, only for the “Contact special dates” feature.
- `USE_EXACT_ALARM`, `SCHEDULE_EXACT_ALARM`: deliver reminders at the exact time, including after snoozing.
- `RECEIVE_BOOT_COMPLETED`: re-register pending reminders after a restart.
- `REQUEST_IGNORE_BATTERY_OPTIMIZATIONS`: only to open the system dialog for the “Reliable delivery” setting.
- `WAKE_LOCK`, `FOREGROUND_SERVICE`, `ACCESS_NETWORK_STATE`: required by the Android system component used for scheduled background work (WorkManager).
`ACCESS_NETWORK_STATE` allows reading *whether* a network connection exists — it does **not** permit using one. Without `INTERNET`, no connection is possible.
`ACCESS_NETWORK_STATE` allows reading *whether* a network connection exists. It does **not** permit using one. Without `INTERNET`, no connection is possible.
## 7. Distribution channels
+7 -10
View File
@@ -9,17 +9,14 @@ Where to look for what:
| [`ARCHITECTURE.md`](ARCHITECTURE.md) | Orientation tour: principles, layers, navigation, recurring-write / conflict / reminder pipelines, testing |
| [`RELEASING.md`](RELEASING.md) | Release process: versioning, the merge-driven pipeline, the two-forge split, secrets, key custody |
| [`../CHANGELOG.md`](../CHANGELOG.md) | Release history (Keep a Changelog, SemVer) |
| [Issues](https://codeberg.org/jlmakiola/calendula/issues) + [milestones](https://codeberg.org/jlmakiola/calendula/milestones) | **The roadmap.** What's planned, in progress, and shipped — a milestone maps to its `release/vX.Y.Z` branch |
| [Issues](https://codeberg.org/jlmakiola/calendula/issues) + [milestones](https://codeberg.org/jlmakiola/calendula/milestones) | The roadmap: what's planned, in progress and shipped. A milestone maps to its `release/vX.Y.Z` branch |
| [`../.planning/PROJECT.md`](../.planning/PROJECT.md) | What the project is: core value, stack + version pins, constraints, naming, forge/release infrastructure |
| [`design/`](design/) | Per-feature design notes kept for features whose provider behaviour is worth recording |
| [`../fastlane/metadata/android/`](../fastlane/metadata/android/) | Store metadata (single source of truth): descriptions, title, icon, screenshots (DE + EN). Harvested directly by the official F-Droid repo; transformed into the self-hosted repo layout at release time by [`../scripts/fastlane_to_fdroid_localized.sh`](../scripts/fastlane_to_fdroid_localized.sh) |
| [`../fastlane/metadata/android/`](../fastlane/metadata/android/) | Store metadata (single source of truth) for every app language, mapped in `store-locales.txt`: descriptions, title, graphics. Harvested directly by the official F-Droid repo, pushed to Google Play with every release, validated by [`../scripts/check_store_listing.py`](../scripts/check_store_listing.py); transformed into the self-hosted repo layout at release time by [`../scripts/fastlane_to_fdroid_localized.sh`](../scripts/fastlane_to_fdroid_localized.sh) |
| [`../fdroid-metadata/`](../fdroid-metadata/) | App-level F-Droid control file (`*.yml`: Categories, License, links) for the self-hosted repo's `fdroid update` |
| [`fdroid-official/`](fdroid-official/) | Recipe + notes for publishing to the **official** F-Droid repo (reproducible build + developer-signed binary) |
| [`fdroid-official/`](fdroid-official/) | Recipe and notes for publishing to the official F-Droid repo (reproducible build + developer-signed binary) |
Conventions: planning lives in the **issue tracker**, not in this repository. The
`.planning/` files that predated it (a roadmap, a development-state snapshot, and
a per-milestone requirement checklist) are gone — issues and milestones say the
same thing without going stale. `PROJECT.md` is what remains, and it describes
the project rather than its plan.
`ARCHITECTURE.md` is the authoritative orientation tour: it is updated with the
code, and is the right place for a lesson learned about the calendar provider.
Planning lives in the issue tracker and its milestones, not in this repository.
`.planning/PROJECT.md` describes the project itself.
`ARCHITECTURE.md` is updated with the code and is the place to record a lesson
learned about the calendar provider.
+24 -14
View File
@@ -162,24 +162,34 @@ before. fastlane appears only as the Play Developer API client (`supply`),
because the store listing already lives in `fastlane/metadata/android/` — the
same tree the official F-Droid repo harvests. One metadata source, two stores.
**What gets uploaded per release:** the AAB, plus the per-version "What's New"
from `fastlane/metadata/android/en-US/changelogs/<versionCode>.txt` — the
**What gets uploaded per release:** the AAB, the per-version "What's New"
from `fastlane/metadata/android/<locale>/changelogs/<versionCode>.txt` (the
hand-written summary from step 3, which is why it must stay **under 500
characters**: Play rejects a longer one. Listing text is
**not** touched — an accidental overwrite of a live listing triggers a Play
policy review. Sync it deliberately with `bundle exec fastlane listing`.
characters**), and the **whole store listing** — title, short and full
description, icon, feature graphic and screenshots for every locale — all in one
Play edit. Nothing about the Play listing is edited in the console any more:
change the files, and the next release pushes them. Unchanged images are
skipped (`sync_image_upload`). Every listing change goes through Play's review,
together with the bundle. To push a listing fix between releases, run
`bundle exec fastlane listing` (add `version_code:<code>` if the track holds
more than one release).
**Screenshots and graphics are skipped**, because the committed assets satisfy
F-Droid but not Play:
The listing tree is guarded by `scripts/check_store_listing.py`, which CI runs
on every pull request:
| Asset | Committed | Play requires |
| --- | --- | --- |
| `phoneScreenshots/*.png` | 1280×2856, 32-bit RGBA | long edge ≤ 2× short edge (so ≤ 2560), 24-bit PNG, no alpha |
| `icon.png` | 512×512, 24-bit RGB | 512×512, 32-bit PNG |
| `featureGraphic.png` | *missing* | required, exactly 1024×500 |
| Rule | Play limit |
| --- | --- |
| `title.txt` / `short_description.txt` / `full_description.txt` | 30 / 80 / 4000 chars, all three present in every locale |
| `full_description.txt` | one line per paragraph — Play renders every newline |
| `images/icon.png` | 512×512, 32-bit PNG |
| `images/featureGraphic.png` | 1024×500, JPEG or 24-bit PNG |
| `images/*Screenshots/*` | JPEG or 24-bit PNG (no alpha), sides 320–3840, long edge ≤ 2× short, 2–8 per type |
| locale directories | exactly the ones in `fastlane/store-locales.txt`, which maps every app `values-*` to a Play locale code |
Until those are fixed, Play's graphics are managed by hand in the console. Then
pass `images:true` to the `listing` lane.
A locale without its own images falls back to en-US in both stores, so graphics
only need to exist there. Adding an app language means adding its line to
`fastlane/store-locales.txt` and its listing directory — CI fails until both
exist. Store text is not on Weblate; it is edited here.
**Track.** Uploads go straight to `production` at a full rollout
(`PLAY_RELEASE_STATUS=completed`). The merge to `main` is already the human