Commit Graph

606 Commits

Author SHA1 Message Date
761b299e6a docs(changelog): record the settings rework (#69)
The whole #69 entry was missing: the reorganised hub, the previewing pickers,
Backup & restore as its own entry, the recurrence date preview and the event
visibility explanations. Also corrects the agenda widget size path, which moved
to Settings → Widgets & tiles in the same rework.
2026-07-30 22:00:30 +02:00
4bdcf20c82 fix: four review findings in the settings rework (#69)
- Restoring a backup left the calendar manager standing over the import
  screen, so the target picker sat hidden behind it.
- The past-events preview consumed drags as well as taps, which stopped the
  picker scrolling when the gesture started on the preview.
- The recurrence preview dropped DTSTART when the rule's weekday picks missed
  the start's own weekday; RFC 5545 keeps it in the set.
- "This month" showed the month name in the range picker, reading as the whole
  month next to options that name a concrete span.

The four files also carry their share of the comment trim from the previous
commit.
2026-07-30 21:57:24 +02:00
9bcd08a5bb docs: trim the code comments back to what the code doesn't say
The v2.17.0 work left long prose comments explaining rationale that either
repeats docs/ARCHITECTURE.md or restates the line below it. Cut ~520 comment
lines across 54 files, keeping the short "why" notes for provider quirks and
non-obvious flow behaviour.
2026-07-30 21:57:15 +02:00
Jean-Luc Makiola
ca2069da0d Sort settings into the places you'd look for them, and say what each one does (#69) (#107)
Reviewed-on: https://codeberg.org/jlmakiola/calendula/pulls/107
2026-07-30 21:25:28 +02:00
Jean-Luc Makiola
02cf32b537 Draw widgets at a size you pick, not a measured one (#103, #51) (#104)
Reviewed-on: https://codeberg.org/jlmakiola/calendula/pulls/104
2026-07-30 19:22:35 +02:00
Jean-Luc Makiola
e44d70e18b Start new events on the selected day in Split month view (#87) (#106)
Reviewed-on: https://codeberg.org/jlmakiola/calendula/pulls/106
2026-07-30 18:58:55 +02:00
Jean-Luc Makiola
401e067945 Reminder delivery review fixes (#75) (#105)
Reviewed-on: https://codeberg.org/jlmakiola/calendula/pulls/105
2026-07-30 17:25:50 +02:00
Jean-Luc Makiola
a0827202b3 Schedule and fire reminders in-house (#75, round two) (#102)
Reviewed-on: https://codeberg.org/jlmakiola/calendula/pulls/102
2026-07-30 16:19:03 +02:00
Jean-Luc Makiola
36bbb3ff2e Four bug fixes for 2.17.0 (#89, #81, #79, #77) (#101)
Reviewed-on: https://codeberg.org/jlmakiola/calendula/pulls/101
2026-07-30 16:18:46 +02:00
Jean-Luc Makiola
1d07b64a28 fix(search): date all-day results in UTC, like every other view (#82) (#88)
Fixes [#82](https://codeberg.org/jlmakiola/calendula/issues/82) — found while investigating [#67](https://codeberg.org/jlmakiola/calendula/issues/67).

Search formatted a result's date with `ZoneId.systemDefault()`, while the month,
week, day, agenda and detail surfaces all resolve an all-day event's dates in
UTC. All-day events are stored at UTC midnight with an exclusive end, so west of
UTC that difference is a whole day: an event on the 19th came back from search
dated the 18th, contradicting the grid it was filed in. East of UTC the offset
lands on the same date, which is why it went unnoticed.

### What changed

The rule was already written down three times — a private helper in
`AgendaUiState`, inline in `coversDay`, inline in `formatWhen` — so rather than
add a fourth copy, `dateZone` / `spanFirstDay` / `spanLastDay` /
`spansMultipleDays` move into `domain/Models.kt`, where they are pure date logic
rather than agenda UI state. Search reads its date through `spanFirstDay`; the
clock time stays in the device zone, as it is only ever rendered for timed events.

### Testing

New `domain/EventInstanceSpanTest` pins both directions: the west-of-UTC case
from this issue and the east-of-UTC leak from #65, plus multi-day spans, timed
events following the device zone, and zero-length events.

Local sweep green: `test` (541), `lint`, `assembleDebug`, `check_translations.py`.

On-device reviewed on the Pixel 10 with the device timezone set to New York
(fix confirmed) and back to Berlin (no regression in search, month, week, day,
agenda or the agenda widget).

---
_Recreated on Codeberg from Gitea PR #102 (same head `7c94425`, unchanged) as part of the forge migration. Already on-device signed off._

Co-authored-by: Jean-Luc Makiola <business@jeanlucmakiola.de>
Reviewed-on: https://codeberg.org/jlmakiola/calendula/pulls/88
2026-07-29 21:47:08 +02:00
d037492cf6 Merge remote-tracking branch 'origin/main' into release/v2.17.0
# Conflicts:
#	CHANGELOG.md
2026-07-29 21:38:42 +02:00
Jean-Luc Makiola
db7094c54e Move the canonical forge to Codeberg (#86)
All checks were successful
Release — F-Droid repo + Gitea/Codeberg release / detect (push) Successful in 14s
Release — F-Droid repo + Gitea/Codeberg release / release (push) Has been skipped
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
2026-07-29 20:33:56 +02:00
c70412c782 Link the privacy policy from the about card (!103)
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
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
2026-07-28 17:10:52 +00:00
8e2109d073 feat(calendars): explain a calendar's state, and why one is missing from the picker (#76, #78) (!101)
Follow-up to #97, on the same release line. After the visibility fix a calendar can be absent from the pickers for three different reasons the app knows and never said; this closes that gap.

**Settings → Calendars names the state** (#76, #78): read-only calendars are marked `Read-only`; ones whose account isn't syncing events to this device are marked `Not synced`, sorted to the bottom of their account, dimmed, and left **without a switch** — visibility can't reveal events that aren't on the device, so the control did nothing.

**Both pickers end in a "Missing a calendar?" row** (#76) opening the calendar manager, where those labels then say which reason applies. The manager overlay moved to the end of the host's overlay stack so it covers every surface that can open it (Settings, both event forms, the import picker).

Review notes:

- **Supporting text, not chips.** A row can carry several states at once next to a live switch; M3 supporting text composes there, static badges don't (and a non-interactive chip reads as a broken button).
- **`sync_events = 0` counts as "not synced" for account-backed calendars only.** Nothing syncs a device-local calendar by definition, and another app's local calendar can hold real events at 0 — the unsoundness that made the first #75 migration guard wrong. Covered by a test.
- **Excluded calendars stay out of the pickers** rather than being listed unpickable — a handful of read-only subscriptions would crowd out the ones you can actually choose.

541 JVM tests green (6 new), lint clean, check_translations clean. **On-device review owed**; the three new string keys need the Weblate backfill.

Reviewed-on: #101
2026-07-26 17:17:02 +00:00
ac0c43f930 Translations update from Weblate (!99)
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
Renovate / renovate (push) Successful in 1m13s
Translations update from [Weblate](https://weblate.dev.jeanlucmakiola.de) for [Calendula/Strings](https://weblate.dev.jeanlucmakiola.de/projects/calendula/strings/).

Current translation status:

![Weblate translation status](https://weblate.dev.jeanlucmakiola.de/widget/calendula/strings/horizontal-auto.svg)

Co-authored-by: Bazyli Cyran <bazyli@cyran.dev>
Co-authored-by: Sup99999 <alelmnour1o234@proton.me>
Reviewed-on: #99
Co-authored-by: Weblate <weblate-bot@jeanlucmakiola.de>
Co-committed-by: Weblate <weblate-bot@jeanlucmakiola.de>
2026-07-26 08:26:54 +00:00
314236ac0c fix(renovate): use a flexible internal-checks filter so ages resolve (!100)
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
Age came back `unknown` for every dependency after #98. That's the `internalChecksFilter: "none"` from that PR doing what it says on the tin — and rather more.

### Why

`lib/workers/repository/process/lookup/filter-checks.ts` short-circuits on `none`:

```js
if (internalChecksFilter === 'none') {
  release = sortedReleases.pop();     // returns here
} else {
  for (let candidateRelease of sortedReleases.reverse()) {
    ...
    const updatedCandidateRelease = await postprocessRelease(...)
```

`postprocessRelease` is the only caller that fetches a Maven artifact's `Last-Modified` header (`lib/modules/datasource/maven/index.ts:222`), so skipping the loop leaves `releaseTimestamp` unset. That empties the Age column — but it also silently voids `minimumReleaseAge` and the `renovate/stability-days` check, since both compare against that same timestamp. The cooling-off tiers were decorative.

### Fix

`flexible` keeps the intent — still prefers a version that has cleared its window, but opens the PR at the newest candidate when none has — while running the loop that populates the timestamps.

Verified with a local `--platform=local --dry-run=full`:

| dependency | new version | age |
| --- | --- | --- |
| `androidx.compose:compose-bom` | 2026.06.01 | 24 d |
| `androidx.compose.material3:material3` | 1.5.0-alpha24 | 10 d |
| `androidx.work:work-runtime-ktx` | 2.11.2 | 122 d |
| `com.android.application` (AGP) | 9.3.1 | 2 d |
| `gradle` | 9.6.1 | 29 d |

Only `ghcr.io/renovatebot/renovate` stays unknown, because the docker registry serves no timestamps at all — the run marks all 176 of its tags pending for that reason, and `flexible` is what still lets that PR through.

`Pending` rejoins the table: under a flexible filter it names the newer version being held back, rather than leaving it invisible.

### Also

Migrates the Gitea workflow manager off the deprecated `fileMatch`. Renovate's config migration was rewriting it to `managerFilePatterns` (delimited regex) on every run and warning about it; the dry run confirms the manager still picks up `.gitea/workflows` afterwards.

Validated with `renovate-config-validator` against the pinned 43.232.0.

Reviewed-on: #100
2026-07-25 20:31:33 +00:00
c6e83fc071 chore(renovate): show real release age and fetch changelogs (!98)
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
Three gaps in the Renovate setup, all about having enough in front of you at review time.

### Age reads UNKNOWN

`config:recommended` already brings in `mergeConfidence:age-confidence-badges`, so the Age column existed — it just rendered grey. Fetching the badge SVGs from `developer.mend.io` directly shows why: Mend's index covers Maven Central but has nothing for Google's Maven repo, so every androidx/compose coordinate is blank. No token changes it; the JSON API behind the badges answers 401 for everyone.

| coordinate | registry | age badge |
| --- | --- | --- |
| `androidx.compose:compose-bom` | Google Maven | UNKNOWN |
| `androidx.core:core-ktx` | Google Maven | UNKNOWN |
| `com.google.truth:truth` | Maven Central | 2y |
| `org.jetbrains.kotlin:kotlin-stdlib` | Maven Central | 1y (confidence high, passing 99%) |

Age is now computed from `newVersionAgeInDays`, which Renovate derives itself to evaluate `minimumReleaseAge` — Google Maven serves `last-modified` on its POMs, so it's populated where Mend is blank, and the number agrees with the tiers it's read against. Mend keeps the Confidence column, which still resolves for the Maven Central half (Kotlin, Gradle, AGP, the test stack).

### Empty release notes

We run against Gitea, but the packages are *released* on GitHub, so changelog lookups were going out unauthenticated against a 60/h limit. `RENOVATE_GITHUB_COM_TOKEN` lifts that.

**Needs a secret before it does anything:** a github.com PAT with **no scopes ticked**, added as repo secret `GITHUB_COM_TOKEN`. Until then the var resolves empty, which is exactly today's behaviour.

### Nothing expressed how settled a release is

Cooling-off scaled by blast radius: 30 days major, 20 minor, 10 patch/digest.

Deliberately advisory. Renovate's default `internalChecksFilter: strict` would suppress the PR outright until the version aged in; `none` opens it at the highest version immediately, so merging ahead of the window stays a decision rather than a wait. A too-young release still gets a yellow `renovate/stability-days` check — `setStability` computes that from `minimumReleaseAge` + `releaseTimestamp` independently of the filter — and with `automerge: false` nothing acts on it.

### Notes

Validated with `renovate-config-validator` against the pinned 43.232.0. Config is read from the default branch, so the open bump PRs keep their current tables until the next run after this merges.

Reviewed-on: #98
2026-07-25 20:09:22 +00:00
bf6415c023 Merge pull request 'fix(calendars): one visibility model, so reminders can't silently go missing (#75)' (!97) from fix/calendar-visibility-model into release/v2.17.0
Reviewed-on: #97
2026-07-25 20:03:14 +00:00
7aef01d95e docs(architecture): record what the second review pass changed
All checks were successful
Translations / check (pull_request) Successful in 5s
CI / ci (pull_request) Successful in 10m57s
The visibility section claimed the reminder side needed no per-calendar
handling. It does on the one path where VISIBLE was never written: an alert the
notifier silences keeps its SCHEDULED state while its event is still ahead, and
switching the calendar back on re-posts it, so the provider's own table is the
stash the deleted SuppressedReminderStore used to be.

Also rewraps the paragraph and separates it from the one that follows, which it
had been running into since the section was added.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 21:38:34 +02:00
ce4d6bc4d1 fix(calendars): keep an event's own calendar when it is switched off
Excluding switched-off calendars from the event form's picker is right for
*targets*, but it also dropped the calendar an event already lives in. Editing
an event of a writable calendar switched off on this device (from a widget, a
deep link, another app's ACTION_EDIT) rendered the calendar row as the red "no
calendar" error, with the picker still enabled — so any pick turned the save
into a calendar move nobody asked for. The event's own calendar is added back
whenever it isn't among the targets, the way the managed special-dates case
already did; a calendar the app may not write to is still no target.

Settings → Notifications had missed the same predicate swap: it kept offering
per-calendar reminder overrides for switched-off calendars, where the provider
schedules no alarms and the setting could never fire.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 21:34:24 +02:00
4ad805e747 fix(calendars): don't flash a flushed calendar's events back on
The reconciler writes VISIBLE=0 and then releases the id from the pending set,
but the ContentObserver that invalidates the repository's cached calendar
snapshot is dispatched through the main looper and arrives later. Until it did,
the released set was read against the snapshot from before the write: the
calendar reported as on again and instances re-admitted exactly the events the
migration was hiding — on a cold start, for as long as the busy main thread took.

The snapshot cache is keyed on the pending set as well as the tick now, so any
read that sees a changed set re-queries the provider; a change to the set also
re-runs the flows, and the pending ids are read in the same pass as the
calendars rather than combined in from a live flow. Both id sets are deduped at
the prefs seam (the store is shared with SettingsPrefs, so every unrelated write
re-emitted them) and calendars() collapses identical lists, which keeps those
re-queries as rare as they should be.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 21:34:24 +02:00
bb6e3ad336 fix(calendars): only tell upgrades about the visibility change, and reconcile on every grant
Two holes in the reconciler, both found by review.

The one-time notice armed on any device holding a calendar at VISIBLE=0, which
is the norm on a fresh install: a second account's calendars, "Holidays in X", a
subscribed calendar. A brand-new user got a changelog dialog about a migration
they never experienced, right after onboarding. It is gated on
firstInstallTime != lastUpdateTime now, and a fresh install retires the notice
unshown ahead of the permission check — so an update installed before the first
grant can't make it look like an upgrade afterwards.

The catch-up run hung off PermissionViewModel.onGranted, which only fires for the
in-app request. Granting from Android's app-settings screen comes back through
RootScreen's ON_RESUME, so an upgrading user who took that route kept their
inherited switch-offs unflushed — their events filtered app-side while the
provider went on scheduling the reminders they asked to stop. The trigger sits on
RootScreen showing the app instead, which covers both routes. To keep that cheap,
a settled run now returns after two DataStore reads instead of querying every
calendar first.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 21:34:24 +02:00
edbeadfa30 fix(reminders): stop marking a silenced reminder handled and losing it
"With VISIBLE=0 the provider creates no alert rows" justified deleting the
suppression stash, but it only holds where the flag was actually written. Where
the switch lives in pendingDisabledCalendarIds — a read-only install, or an
upgrade whose flush hasn't landed — the provider still holds VISIBLE=1 and keeps
creating and broadcasting rows. The receiver silenced those in
ReminderNotifier.post and then marked the whole due batch STATE_FIRED, and
dueAlerts only ever returns STATE_SCHEDULED, so switching the calendar back on
before the event could no longer surface the reminder: it was gone.

post() now reports whether it put a notification up, and the receiver marks what
it posted plus what it silenced for an event already over (handledAlertIds). A
silenced alert for an event still ahead stays scheduled, which makes the
provider's own table the stash SuppressedReminderStore used to be — no local
mirror, no serialization. ReminderRecovery re-posts those rows when the calendar
is switched back on, so recovery doesn't wait for the next unrelated broadcast.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 21:33:58 +02:00
ef48717e2c fix(calendars): hide-only visibility reconcile, and keep it working read-only
Code review of the one-visibility-model fix (#75) found the reconciliation
reaching further than it should and the read-only case falling through it.

The migration switched calendars *on* to keep the upgrade invisible, but
"not disabled in Calendula" is the default for every calendar, including ones
the user deliberately hid in Google Calendar, Etar or DAVx5 — those would
reappear there and start firing reminders from a switch the user never touched.
Its sync_events guard didn't hold either: an ACCOUNT_TYPE_LOCAL calendar another
app created can sit at sync_events=0 while holding real device-local events. The
reconcile now only hides, and a one-time notice explains that visibility follows
the device and where to change it, instead of quietly rewriting other apps'
state.

Only READ_CALENDAR gates the app, so a read-only install could not write the
flag at all: every calendar it had switched off came back with its events and
its reminders, and the switch couldn't undo it. Those switch-offs are kept
app-side now (the retired disabled-set key, re-read under a new name), folded
into the visibility every consumer reads, and drained into the provider entry by
entry once WRITE_CALENDAR arrives — which also makes a part-applied run resumable
without re-applying a switch the user has since flipped by hand.

Also: restore the ReminderNotifier.post gate, the one path a snooze re-shown
from our own alarm passes; move the whole reconcile inside its try/catch, so a
damaged preferences file can't crash the process at launch; share one Calendars
query per provider tick across the flows that need it; and give the reworded
Settings hint new keys, so five locales stop rendering the retired app-only
wording.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 13:16:55 +02:00
41593a0e9d fix(calendars): make the Settings toggle the one visibility model (#75)
Reminders never fired for a calendar hidden at system level and nothing hinted
at it: the provider only schedules reminder alarms for Calendars.VISIBLE=1,
while Calendula filtered with its own disabledCalendarIds pref and parsed
isVisibleInSystem without ever using it — two models that could disagree
indefinitely.

Settings → Calendars now writes Calendars.VISIBLE, one calendar per update
(CalendarProvider2 skips its own checkNextAlarm() reschedule for any selection
that isn't _id=), and every display predicate reads isVisibleInSystem. The
drawer's filter sheet stays a purely in-app declutter and still leaves reminders
alone.

With VISIBLE=0 the provider creates no alert rows, so there is nothing left to
suppress: the disabled-calendar gates, SuppressedReminderStore and the re-enable
recovery are gone. A one-shot migration reconciles the retired set with the app's
state winning — enabled in-app and syncing gets shown, disabled gets hidden,
everything else untouched — so the upgrade changes nothing the user sees.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 12:44:56 +02:00
4c1bfc052e Merge pull request 'ci(release): retry the Codeberg release create past the tag-settle 500' (!96) from fix/codeberg-release-retry into main
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
Reviewed-on: #96
2026-07-25 07:09:53 +00:00
bba536394f ci(release): retry the Codeberg release create past the tag-settle 500
All checks were successful
Translations / check (pull_request) Successful in 6s
CI / ci (pull_request) Successful in 4m23s
The Codeberg mirror step pushes the tag, then immediately POSTs the release
for it — but Codeberg 500s when the release request outruns its indexing of
the just-pushed ref, and with only one attempt that single 500 skipped the
mirror every release (the same POST succeeds seconds later, as a manual retry
confirmed for v2.16.0). Wrap the create/update in a backoff retry loop that
PATCHes in place if a release already exists, so a transient 5xx no longer
loses the mirror. Step stays best-effort (continue-on-error).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 22:22:52 +02:00
bb34973047 Merge pull request 'Release 2.16.0' (!95) from release/v2.16.0 into main
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 12m59s
Reviewed-on: #95
v2.16.0
2026-07-24 20:00:13 +00:00
108a1890be docs(changelog): cut 2.16.0
All checks were successful
Translations / check (pull_request) Successful in 6s
CI / ci (pull_request) Successful in 10m49s
Consolidate the release under a single 2.16.0 heading dated today and fold
in the work that had accumulated in Unreleased. Add the entries the log had
been missing as PRs merged: app-name toggle (#44), the Arabic/French/Italian
translations, the custom recurrence picker redesign + end-date fix (#42),
multi-day agenda events (#83), and dark-theme picker bar icons (#70).
Regenerate the fastlane per-version changelog.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 21:47:44 +02:00
521c0ffd78 i18n: offer Arabic in the language picker
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>
2026-07-24 18:20:07 +02:00
33a37d4105 Merge remote-tracking branch 'origin/main' into release/v2.16.0 2026-07-24 18:17:23 +02:00
6ccc0cbed2 Merge pull request 'Translations update from Weblate' (!91) from weblate-bot/calendula:weblate-calendula-strings into main
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-24 16:15:27 +00:00
Weblate
6e6ffce271 Update translation files
All checks were successful
Translations / check (pull_request) Successful in 6s
CI / ci (pull_request) Successful in 4m28s
Updated by "Remove blank strings" add-on in Weblate.

Translation: Calendula/Strings
Translate-URL: https://weblate.dev.jeanlucmakiola.de/projects/calendula/strings/
2026-07-24 16:09:17 +00:00
Sup99999
23386bb998 Translated using Weblate (Arabic)
Currently translated at 52.4% (229 of 437 strings)

Translation: Calendula/Strings
Translate-URL: https://weblate.dev.jeanlucmakiola.de/projects/calendula/strings/ar/
2026-07-24 16:09:17 +00:00
Xavier
28b1568486 Translated using Weblate (French)
Currently translated at 91.0% (398 of 437 strings)

Translation: Calendula/Strings
Translate-URL: https://weblate.dev.jeanlucmakiola.de/projects/calendula/strings/fr/
2026-07-24 16:09:17 +00:00
Sup99999
5127971a37 Translated using Weblate (Arabic)
Currently translated at 32.7% (143 of 437 strings)

Translation: Calendula/Strings
Translate-URL: https://weblate.dev.jeanlucmakiola.de/projects/calendula/strings/ar/
2026-07-24 16:09:17 +00:00
Weblate
0562af8d66 Update translation files
Updated by "Remove blank strings" add-on in Weblate.

Translation: Calendula/Strings
Translate-URL: https://weblate.dev.jeanlucmakiola.de/projects/calendula/strings/
2026-07-24 16:09:17 +00:00
Sup99999
e3d3728bbd Added translation using Weblate (Arabic) 2026-07-24 16:09:17 +00:00
Weblate
8cc9d075fd Translated using Weblate (Italian)
Currently translated at 100.0% (437 of 437 strings)

Translation: Calendula/Strings
Translate-URL: https://weblate.dev.jeanlucmakiola.de/projects/calendula/strings/it/
2026-07-24 16:09:17 +00:00
Sam Gamgee
ede6d967c1 Translated using Weblate (Italian)
Currently translated at 100.0% (437 of 437 strings)

Translation: Calendula/Strings
Translate-URL: https://weblate.dev.jeanlucmakiola.de/projects/calendula/strings/it/
2026-07-24 16:09:17 +00:00
c30f6ab318 Merge pull request 'build(lint): don't fail the build on partial community plurals' (!94) from fix/lint-partial-plurals into main
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-24 16:09:12 +00:00
dd2b96b5fd build(lint): don't fail the build on partial community plurals
All checks were successful
Translations / check (pull_request) Successful in 6s
CI / ci (pull_request) Successful in 9m49s
A Weblate translation of a <plurals> may not fill every CLDR quantity
form its locale defines — e.g. Arabic's import_reminder_prompt_body_existing
omits the "zero" form — which lint flags fatally as MissingQuantity and
aborts the CI build. The missing form falls back to "other" at runtime, so
this is the same partial-translation case we already accept for
MissingTranslation; downgrade MissingQuantity to informational alongside it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 17:57:51 +02:00
c22658db4c Merge pull request 'fix(intents): show Calendula under "Add to calendar" from other apps (#74)' (!93) from fix/insert-event-intent-filters into release/v2.16.0
Reviewed-on: #93
2026-07-24 15:46:13 +00:00
7b3893ddc6 feat(intents): open .ics/.vcs handed over as application/octet-stream (#74)
All checks were successful
Translations / check (pull_request) Successful in 5s
CI / ci (pull_request) Successful in 10m43s
Mail clients, browsers and file managers frequently label a calendar
attachment as a generic download (application/octet-stream) rather than
text/calendar, so the MIME-typed VIEW filter missed them. Add a dedicated
filter that matches those by .ics/.vcs extension via pathPattern — kept
separate so the path constraint can't narrow the reliable MIME-typed filter.
The import handler already ignores the declared MIME, so a let-through file
imports normally. Best-effort: reliable for file:// and content:// whose path
carries the name; nameless content:// URIs still fall back to the MIME filter.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 17:40:27 +02:00
9e28fa4981 fix(intents): accept item-typed INSERT / INSERT_OR_EDIT for "Add to calendar" (#74)
Other apps' "Add to calendar" / "Save to calendar" actions commonly fire the
canonical insert intent — ACTION_INSERT with setType("vnd.android.cursor.item/
event") — the singular *item* MIME type (Android's own docs example; used by
e.g. DB Navigator). Calendula advertised INSERT only on the *dir* MIME type, so
it never matched: absent from the chooser, and a silent no-op when it was the
only calendar app installed.

Add the item-typed INSERT to the events filter, plus ACTION_INSERT_OR_EDIT (the
third "add to calendar" action AOSP and Google Calendar register). The runtime
parser already treats any ACTION_INSERT as create; teach insertFormOrNull /
editEventKeyOrNull about INSERT_OR_EDIT so an id-less one is a create and an
id-carrying one opens the edit form.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 17:37:59 +02:00
7549c8fe04 Merge pull request 'fix(pickers): keep full-screen picker bar icons theme-matched (#70)' (!92) from fix/picker-status-bar-icons into release/v2.16.0
Reviewed-on: #92
2026-07-24 15:26:24 +00:00
75f699b714 chore(floret-kit): re-point submodule to merged main (#70)
All checks were successful
Translations / check (pull_request) Successful in 6s
CI / ci (pull_request) Successful in 2m32s
floret-kit#3 merged to main; move the pointer from the fix branch commit
to the main merge commit. No code change — same tree.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 17:24:57 +02:00
3e821c9092 fix(pickers): keep full-screen picker bar icons theme-matched (#70)
All checks were successful
Translations / check (pull_request) Successful in 6s
CI / ci (pull_request) Successful in 9m26s
Bump floret-kit to the fix that drives each picker Dialog window's
status/navigation-bar icon appearance from the runtime theme. The picker
Dialog owns its own Window, seeded from the XML theme rather than the
activity's runtime edge-to-edge state, so in dark theme the status-bar
icons went black and near-invisible on the dark picker (Codeberg #70).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 17:12:29 +02:00
7602f158a3 Merge pull request 'feat(month): month view style picker + Split pull-to-expand (#38, #53)' (!90) from feat/month-view-style into release/v2.16.0
Reviewed-on: #90
2026-07-20 21:25:49 +00:00
052335e842 fix(month): carry the scroll position across a style change (#38)
All checks were successful
Translations / check (pull_request) Successful in 5s
CI / ci (pull_request) Successful in 5m54s
The scrolling styles navigate by scroll position and never set the paged
month, so it sat wherever paged navigation last left it — today's month
on a fresh open. Switching to a paged style, or reseeding the other
scrolling style's list state (both read off it), snapped back there
instead of holding the month on screen.

Track the paged month to the visible month while a scrolling style is
up, realigning the split selection alongside it so landing on Split
shows a live day in the month you were viewing rather than a stale,
off-month one.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 23:20:34 +02:00