Jean-Luc Makiolaandmakiolaj f55cfffa3a
Beta — Codeberg pre-release / detect (push) Successful in 5s
Beta — Codeberg pre-release / beta (push) Skipped
Extract repeated helpers in data, domain and UI layers (#416)
Step 6 of the simplification review: mechanical helper extractions, one commit per group. Widget code is untouched.

**Data layer** (CalendarDataSource and the mappers)
- `updateEventRow`/`eventUri` replace the seven "update, throw if no row" copies and the loose `withAppendedId` spellings.
- `SeriesRow.timeColumns()` / `exdateValues()` define the DTSTART/RRULE/DURATION/TZ/ALL_DAY set once for truncate, split, detach, delete and exception copy.
- One `insertReminderRow` for the four reminder loops, `insertLocalCalendar` for local and managed creates, `applyManagedCalendarReminders` iterates `queryManagedEvents`, `reconcileAttendees` reuses `insertAttendees`, plus `mapRows`, `instancesUri`, `mintUid`, shared `eventColorColumns` and `switchedOffCalendarIds`.
- The read-side mappers share `ColumnReader` helpers (end time, status, all-day UTC date); each keeps its own end-time clamp. `importDuration` goes through `rfc2445Duration` with identical output. The `eventColorPalette` data-source member is gone; the repository calls the pure function.
- One behaviour difference: a provider *exception* while inserting a reminder row was only swallowed on import. It is now logged and skipped on every path (create, update, reconcile, move), as the existing comments already describe it, so it no longer fails the event write.

**Domain**
- `BasicFormat.kt` replaces the four `%04d…` formatters and three substring parsers (ICS writer/parser, Recurrence, RecurrenceText). Parsing stays as lenient as before.
- `rruleParts()` for the RRULE tokenising in five places; each caller keeps its own malformed-part handling. `rruleTruncatedAt` keeps its own tokeniser since it preserves garbage parts verbatim.
- `allDayFormTimes` and `normalizedRrule` for the three form-prefill copies, `zoneOrNull` for the eight zone guards, EventSearch orders months by `YearMonth`, IcsParser uses duration arithmetic and `firstOrNull`.
- FREQ matching no longer depends on the device locale (a lowercase rule failed to parse on Turkish-locale phones).
- Skipped: the contact-date parser rewrite (tests don't pin enough inputs) and the writer-side enum tables (their exhaustive `when` is a useful compile check).

**UI / view models**
- `dayRange(first, last, zone)` and one `startOfWeek` replace four start..end-of-day helpers and two identical week-start functions.
- `orUnnamed()` replaces 17 `ifBlank { calendar_unnamed }` spellings; a shared `CalendarVisibilitySwitch` and `CalendarManagerRow`.
- Filter uses `CalendarAccountGroup` plus a hidden-id set instead of its own copy of the grouping model; the grouping test moved to `AccountGroupsTest`.
- `agendaDays` is shared by the Agenda screen and the split-month pane.
- kotlinx-datetime's `yearMonth`, `firstDay`/`lastDay` and java.time converters replace about 20 hand-written bridges; one `formatTimeOfDay` for agenda rows and month chips.
- No visible UI change.

Net about −240 lines.

Co-authored-by: Jean-Luc Makiola <business@jeanlucmakiola.de>
Reviewed-on: https://codeberg.org/jlmakiola/calendula/pulls/416
2026-10-16 13:56:17 +02:00
2026-10-01 16:21:11 +02:00
2026-10-07 20:22:50 +02:00
2026-10-01 16:21:11 +02:00
2026-10-07 09:56:00 +02:00
2026-09-02 12:44:40 +02:00
2026-10-01 16:21:11 +02:00
2026-10-07 09:56:00 +02:00
2026-10-07 09:56:00 +02:00

Calendula icon

Calendula

A Material 3 Expressive calendar for Android.
It works on top of the system calendar and has no network access.

CI Android 10+ Kotlin + Compose Material 3 Expressive MIT License

Get it on F-Droid   Get it on Google Play   Get it on Obtainium   Support me on Ko-fi

Week view  Month view  Agenda view  Event detail  Editing a recurring event  Calendars from your accounts  Dark theme

Calendula is named after the flower. Both its name and the word calendar come from the Latin kalendae, the first day of the month.

The app works directly on Android's CalendarContract. Any calendar synced to your device (CalDAV via DAVx5, Google, local, WebCal subscriptions) shows up in Calendula, and whatever you create or edit syncs back the same way. Calendula has no database or sync code of its own.

Features

Calendar

  • Month, week and day views, with a switcher in the top bar
  • Event details with attendees and their responses, reminders, recurrence in plain words, availability, visibility and foreign time zones
  • Calendars can be shown or hidden one by one, grouped by account
  • Your contacts' birthdays, anniversaries and other dates, if you turn it on. Contacts are read on the device and never changed.

Editing

  • Create, edit and delete events. For a recurring event you choose whether a change applies to only this event, this and all following, or the whole series.
  • The repeat picker has presets and custom rules (interval, weekdays, end condition). Rules it can't show are kept as they are.
  • If an event changed elsewhere while you were editing it, Calendula asks before saving over it.
  • Read-only calendars such as WebCal subscriptions and birthdays are detected and can't be edited by accident.

Reminders

  • Calendula shows event reminders as notifications itself. Android leaves reminder delivery to calendar apps, so if Calendula is your only one, this is where your reminders come from.
  • Tapping a reminder opens the event.

Design and privacy

  • Material 3 Expressive throughout, with dynamic color on Android 12+, light and dark themes, and expressive motion and shapes
  • Available in 16 languages, with a per-app language setting. Help translate.
  • No telemetry, no analytics and no internet permission. Your data stays on the device.

Install

All channels install the same app:

Channel Updates Notes
Official F-Droid On F-Droid's build schedule Recommended; no extra setup
Self-hosted F-Droid repo Minutes after a release Fastest; add the repo once
Codeberg release / Obtainium Per release Plain APK download, or automatic with Obtainium
Google Play Per release Signed with Google's key, see below
Build from source Whenever you build Full control

Calendula is in the official F-Droid repository. Search for Calendula in any F-Droid client, or install it from f-droid.org.

F-Droid builds from source on its own schedule, so a new version usually shows up there a few days after release.

Self-hosted F-Droid repo (fastest updates)

The release pipeline builds, signs and publishes every release to a self-hosted F-Droid repository, so new versions land there first. Add it once and your F-Droid client handles updates from then on:

  1. In your F-Droid client, open Settings → Repositories → Add (or open the link below on your phone):

    https://apps.dev.jeanlucmakiola.de/dev/fdroid/repo?fingerprint=C2C0640402BF458FC0ED957AF0B37AA4C14022E72F89CE90B5965B458CF73425
    

    Repo: https://apps.dev.jeanlucmakiola.de/dev/fdroid/repo · fingerprint (SHA-256): C2C0 6404 02BF 458F C0ED 957A F0B3 7AA4 C140 22E7 2F89 CE90 B596 5B45 8CF7 3425

  2. Refresh, search for Calendula and install it.

Codeberg release / Obtainium

Every release is also on Codeberg with the signed APK (calendula_vX.Y.Z.apk) and a .sha256 checksum. Download it and install it directly.

For automatic updates from Codeberg, use Obtainium. On your phone, add Calendula in one tap, or add it by hand: Add App, paste https://codeberg.org/jlmakiola/calendula, then Add. Obtainium then watches the releases and tells you when a new one is out. Betas of upcoming versions are published there too, as pre-releases; to test them, switch on Include prereleases for Calendula in Obtainium.

Google Play

Calendula is on Google Play.

Play builds are signed with Google's key rather than mine, so switching between Play and any other channel means uninstalling first. You lose the app settings. Your events live in the system calendar and aren't affected.

Build from source

It's a plain Gradle build with no proprietary dependencies. See docs/BUILDING.md, and note the floret-kit submodule.

Official F-Droid, the self-hosted repo and the Codeberg releases share the same signing key, so you can switch between them without reinstalling. Google Play is the exception (see above).

Documentation

Contributing

Bug reports, ideas and patches are welcome on Codeberg.

Start with an issue. A feature gets a yes or no before anyone writes code, and features and bugs are assigned a milestone whose release/vX.Y.Z branch your pull request targets. Typo and docs fixes can go straight to a pull request. Translations go through Weblate, not pull requests.

Read CONTRIBUTING.md before writing code. It covers the workflow, the build (including the floret-kit submodule) and the architectural rules changes are reviewed against.

Translations

Calendula is written in English. Translators on Weblate have added Arabic, Belarusian, Chinese (Simplified), Czech, French, German, Hungarian, Italian, Japanese, Polish, Portuguese (Portugal and Brazil), Russian, Slovak and Spanish, some of them partly. Untranslated strings fall back to English, so a partial translation is fine. Translations are managed on a self-hosted Weblate:

Help translate Calendula

You don't need to code. Register on the Weblate server, pick a language (or request one) and translate in your browser. The app links here from the top of Settings → App language.

Privacy

Calendula has no analytics, tracking, ads, crash-reporting SDK or third-party services. It doesn't have the internet permission, so it can't send anything anywhere. Your events stay in Android's own calendar store.

Privacy policy, written in docs/PRIVACY.md, which is what that page renders.

License

MIT, Jean-Luc Makiola, 2026

S
Description
Build infrastructure only — canonical repo: https://codeberg.org/jlmakiola/calendula
Readme MIT
17 MiB
v2.22.5
Latest
2026-10-07 18:37:06 +00:00
Languages
Kotlin 98.5%
Shell 0.9%
Python 0.5%
Ruby 0.1%