Found the actual cause. Fossify mirrors Contacts birthdays and
anniversaries with startTS == endTS (MainActivity), so its exporter's
dayCode(endTS + 12h) rounds back to the starting day and writes
DTEND == DTSTART. We read that literally: a yearly series with
DURATION:P0D, which the provider expands into no instances at all. The
import reported success and the events were nowhere — matching the
report exactly ("birthdays, memorials, dates").
An all-day event may no longer end at or before it starts, and an all-day
DURATION is floored at P1D. Verified against the released parser, which
returns days=0.0 for all three fixture events.
Do not generalise this into sniffing PRODID: the same exporter is correct
for UI-created events (endTS anchors at noon of the last day) and for
CalDAV rows, and Fossify's bundled holiday files are conformant while
naming Fossify in their PRODID. Both are kept as fixtures.
Also: existingUids counted DELETED rows, so re-importing after deleting
events skipped everything as duplicates.
Closes#225
importEvents inserted every event in one unguarded loop, so a single row
the provider refused — it throws on a malformed RRULE straight out of
insert — aborted the batch and left the user with "couldn't read this
file" and nothing imported. Each event is now isolated and rejects are
counted into IcsImportSummary.failed and shown. sanitizeRrule drops empty
and malformed rule parts before the write.
Alongside that, several things a foreign file carries that we dropped:
VTODO components (silently lost, now imported as events), EXDATE, the
CATEGORIES calendar name, and colours — X-FOSSIFY-EVENT-COLOR / COLOR /
the category colour plus the X-SMT-* legacy spellings, snapped in Oklab
to the nearest key a palette account publishes since those reject a raw
EVENT_COLOR.
Two correctness fixes on our side: an all-day event may no longer end at
or before it starts (the provider expands a zero-length series into no
instances, so it just disappears), and imported all-day reminders now go
through the same encoding as hand-created ones instead of firing at UTC
midnight. A VALARM trigger pointing after the start is read as a time of
day rather than clamped to zero.
Note for later: their all-day DTEND is RFC-correct — endTS anchors at
noon of the last day and the exporter's +12h rounds it to the following
midnight. Do not "fix" it by sniffing PRODID; the holiday files bundled
in Fossify are conformant and name Fossify in theirs. One is kept as a
fixture.
Refs #225
Release 2.19.2 — `versionName` 2.19.2 / `versionCode` 21902. Merging this cuts the release.
A meeting you declined is struck through everywhere it appears and no longer schedules a reminder (#180). Tapping an event in month view opens the event instead of its day, in every month style (#187). An edit shows immediately when the same occurrence is re-opened (#196).
The month widget's arrows died after a couple of taps: the grid serialised ~740 views and one update ran to 450–540 KB, which overran the launcher's async binder buffer and made Android drop the whole widget host — killing updates for every widget on the home screen. The grid is down to 192 views, and `SizeMode.Exact` is kept so a resized widget reflows instead of clipping (#214). Widget taps now redraw the tapped widget by its own id rather than `updateAll`, whose provider lookup is empty in a cold-woken process (#18).
Jump-to-today in seamless weeks lands on the current week row (#191), and the day and week timelines got an end inset (#192).
Two follow-ups beyond the issues: the detail re-read keeps the loaded content on screen instead of blinking back to the skeleton, and a month chip tap with no recorded touch down (TalkBack, D-pad) no longer resolves to the previous tap's chip.
Release notes: `CHANGELOG.md` 2.19.2 section and `fastlane/metadata/android/en-US/changelogs/21902.txt` (471 chars).
Closes#18Closes#180Closes#187Closes#191Closes#192Closes#196Closes#214
Co-authored-by: Jean-Luc Makiola <business@jeanlucmakiola.de>
Reviewed-on: https://codeberg.org/jlmakiola/calendula/pulls/216
Release branch for 2.19.1. Merging this cuts the release — versionName is bumped to 2.19.1 (versionCode 21901), which is what release.yaml picks up.
### What's in it
- **Drag to reschedule can be turned off** (#173). Settings → Views → *Drag to reschedule*, on by default. Turned off, no event block in the month, week or day view registers a drag gesture. The gate is `EventMoveScope.dragEnabled` rather than the scope itself, so the screen-reader "Move event" action — which only opens the edit form — stays available either way. The setting reads as off until the stored value has actually loaded, so a cold start can't hand out a drag before the preference is known.
### Release prep
- CHANGELOG 2.19.1 section written.
- Per-version "What's New" hand-written for all 12 store locales (`ar`, `de-DE`, `en-GB`, `en-US`, `es-ES`, `fr-FR`, `it-IT`, `pl-PL`, `pt-BR`, `pt-PT`, `ru-RU`, `zh-CN`).
- `lint test assembleDebug` and `check_translations.py` green locally.
- main is one commit ahead (Weblate #175, translations only) and merges clean — not merged into the branch, it comes along with this one.
Closes#173
Co-authored-by: Jean-Luc Makiola <business@jeanlucmakiola.de>
Reviewed-on: https://codeberg.org/jlmakiola/calendula/pulls/176
- Back now closes the drawer instead of the app: a `BackHandler` inside the sheet, enabled while the drawer is open. The four host screens (Month/Week/Day/Agenda) pass their `drawerState` in.
- Header and section labels use `GroupedListInset` instead of a hardcoded 28.dp, so "Calendula", "View" and "Calendars" share the left edge of the rows below them — same as the Settings screens. The account labels and placeholders in `CalendarFilterList` follow.
- Plain leading icons now sit in a 40.dp box, matching `CalendarColorChip`'s footprint, so view rows, jump-to-date, Settings and the calendar rows all line up on one vertical axis.
Closes#114
Co-authored-by: Jean-Luc Makiola <business@jeanlucmakiola.de>
Reviewed-on: https://codeberg.org/jlmakiola/calendula/pulls/121
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.
**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 · floret-kit moved
Mirrored to `jlmakiola/floret-kit` (6 branches, 3 tags, every SHA verified identical) and `.gitmodules` repointed. This is what actually unblocks contributors — a clone previously could not resolve its submodule without reaching the personal Gitea instance.
The Gitea copy is **kept**: every existing tag records the old submodule URL, so rebuilds of past releases (including F-Droid reproducible rebuilds) still resolve.
### 2 · 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.
### 3 · Two release-pipeline safety changes
- `release.yaml`'s `detect` gets 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 the release pipeline 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.
### 4 · Links repointed
In-app Source/License links, README badge, both F-Droid metadata files. **`Repo:` in `docs/fdroid-official/` deliberately stays on Gitea** — it keeps receiving `main` and every tag, so it remains a complete build source, and leaving it alone means no fdroiddata MR and no reproducible-build risk.
### Not in this PR
Renovate + Weblate repointing, and the Codeberg → Gitea push mirror (browser-side).
Supersedes Gitea PR #104.
Co-authored-by: Jean-Luc Makiola <business@jeanlucmakiola.de>
Reviewed-on: https://codeberg.org/jlmakiola/calendula/pulls/86
Adds a fourth link to the about card in settings, next to source and licence.
Play requires the privacy policy to be reachable from inside the app, not just from the store listing, for anything that touches calendar or contact data. Points at jeanlucmakiola.de/calendula/privacy.
Only the base string is added — Weblate owns the translations.
Reviewed-on: #103
The Weblate merge added a values-ar translation (~50% complete, RTL —
the app already declares supportsRtl). Add the matching <locale> line so
Arabic is selectable in both the in-app picker and the system per-app
language settings. Russian and Portuguese stay out for now: at 12% and
<1% they're below the usable bar (the lowest currently-offered language,
zh-CN, sits at ~24%).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>