Commit Graph

195 Commits

Author SHA1 Message Date
ac572da40a build(fdroid): drop unused foojay toolchain resolver; release v2.7.4
All checks were successful
CI / ci (pull_request) Successful in 8m50s
F-Droid's official build scanner rejects the foojay-resolver-convention
plugin because it can fetch a JDK over the network during the offline,
reproducible build. The plugin was inert here (no toolchain block ever
invoked it), so removing it leaves the app functionally identical to
2.7.3 and still reproducible byte-for-byte from source.

Bumps versionName to 2.7.4 (the release trigger) and retargets the
official fdroiddata recipe draft at v2.7.4, plus two recipe corrections
the fdroiddata CI caught on the v2.7.3 submission (MR !40967):
- Categories: 'Time' (retired in the current taxonomy) -> 'Calendar & Agenda'
- AutoUpdateMode: 'Version v%v' (invalid per schema) -> 'Version'

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 20:09:31 +02:00
f27811d215 Merge pull request 'chore(fdroid): finalize recipe at v2.7.3 + reproducible-release CI guard' (#23) from chore/fdroid-recipe-2730 into main
All checks were successful
Release — F-Droid repo + Gitea release / detect (push) Successful in 4s
Release — F-Droid repo + Gitea release / release (push) Has been skipped
Reviewed-on: #23
2026-06-21 12:59:56 +00:00
36e2199dff chore(fdroid): finalize recipe at v2.7.3 + reproducible-release CI guard
All checks were successful
CI / ci (pull_request) Successful in 8m50s
v2.7.3 is the first release built with `vcsInfo { include = false }` and is
verified reproducible: a clean from-source build matched the published
calendula_v2.7.3.apk byte-for-byte across all 1382 zip entries (only the
signature block differs, which F-Droid copies).

- Point the fdroiddata recipe's Builds entry + CurrentVersion at v2.7.3
  (was the 2.7.2 placeholder, which lacks the fix and would fail verification).
- UpdateCheckMode: Tags ^v[0-9.]+$ — after this one-time submission F-Droid
  auto-tracks new release tags and adds build entries itself; no manual recipe
  edits per release.
- Add scripts/check_reproducible_release.sh + an always-on CI step asserting the
  release build keeps VCS-info disabled, so reproducibility can't silently
  regress and quietly stop official-repo publishing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 14:28:01 +02:00
eb51175ceb Merge pull request 'fix(widget): widgets stuck on loading spinner in release build (#18)' (#22) from fix/widget-release into main
All checks were successful
Release — F-Droid repo + Gitea release / detect (push) Successful in 4s
Release — F-Droid repo + Gitea release / release (push) Successful in 7m8s
Reviewed-on: #22
v2.7.3
2026-06-21 12:15:44 +00:00
bc59200f77 fix(widget): keep WorkManager InputMerger so widgets render under R8 (#18)
All checks were successful
CI / ci (pull_request) Successful in 8m45s
Home-screen widgets were stuck on the Glance loading layout (a blank spinner)
in the minified release build — but worked in debug. Root cause: Glance renders
every widget through a WorkManager worker (androidx.glance.session.SessionWorker),
and WorkManager instantiates the InputMerger reflectively from the fully-qualified
class name persisted in the WorkSpec (Class.newInstance, no-arg ctor). Under R8
full mode (AGP 9 default) the unused no-arg constructor of
androidx.work.OverwritingInputMerger was stripped, so WorkManager threw
"OverwritingInputMerger has no zero argument constructor", the SessionWorker
never ran, and provideContent never executed — leaving the widget on its
initial loading layout forever.

Same R8-reflection family as the v2.7.0 Room keep-rule fix. Keep the name +
constructor of every androidx.work.InputMerger.

Verified on-device with the releaseTest build (R8-minified): the agenda widget,
which showed only a spinner before, now renders its content; the WM-InputMerger
InstantiationException is gone. Closes #18. Cuts v2.7.3.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 14:06:34 +02:00
ed6b0fa89f Merge pull request 'chore(fdroid): single-source fastlane metadata + reproducible-build prep' (#21) from chore/release-verification-process into main
All checks were successful
Release — F-Droid repo + Gitea release / detect (push) Successful in 5s
Release — F-Droid repo + Gitea release / release (push) Has been skipped
Reviewed-on: #21
2026-06-21 11:38:20 +00:00
c503199e06 ci: make change-scope classification robust to divergent PR branches
All checks were successful
CI / ci (pull_request) Successful in 4m24s
The scope step shallow-fetched the base (`--depth=1`) and ran
`git diff origin/<base>...HEAD`. The three-dot diff needs the merge-base,
which a depth-1 tip doesn't contain once a branch has forked a few commits
back — git aborts with "no merge base" (exit 128) and the whole CI job
fails before lint/test/build ever run.

Fetch the base fully, resolve the merge-base explicitly, and diff from it.
If no common ancestor is found, default to code=true so the build still
runs rather than being wrongly skipped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 13:32:30 +02:00
f3f155b5ff chore(fdroid): single-source fastlane metadata + reproducible-build prep
Some checks failed
CI / ci (pull_request) Failing after 8s
Make fastlane/metadata/android/ the single source of truth for store
listing metadata, consumed directly by the official F-Droid repo and
transformed into the self-hosted repo's localized layout at release time.

- Move fdroid-metadata/<appid>/<locale>/ -> fastlane/metadata/android/<locale>/
  (git-tracked renames: summary->short_description, description->full_description,
  + title.txt, images/icon.png, images/phoneScreenshots/); keep the app-level
  .yml control file for the self-hosted `fdroid update`.
- Add scripts/fastlane_to_fdroid_localized.sh (fastlane -> F-Droid localized,
  incl. changelogs) and scripts/sync_changelog_to_fastlane.sh (CHANGELOG.md ->
  fastlane changelog); verified byte-identical to the previous metadata.
- release.yaml: build self-hosted metadata from the fastlane tree and sync the
  per-version changelog before the transform (one changelog source for both
  channels).
- Disable AGP VCS-info embedding on release builds (vcsInfo { include = false })
  so builds reproduce byte-for-byte vs the distributed APK — the only file that
  otherwise differed (META-INF/version-control-info.textproto). Effective from
  the next release.
- Add docs/fdroid-official/ (draft fdroiddata recipe: reproducible build +
  AllowedAPKSigningKeys + Binaries + notes).
- Repoint README screenshots/icon, update docs/README + RELEASING, and skip the
  Android build on fastlane-only changes (ci.yaml).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 13:17:21 +02:00
294524a943 Merge pull request 'release: v2.7.1 — privacy-respecting crash reporting' (#9) from feat/crash-report into main
All checks were successful
Release — F-Droid repo + Gitea release / detect (push) Successful in 4s
Release — F-Droid repo + Gitea release / release (push) Successful in 5m39s
Reviewed-on: #9
v2.7.2
2026-06-21 11:13:25 +00:00
ef6f1891b3 Merge remote-tracking branch 'origin/main' into feat/crash-report
All checks were successful
Translations / check (pull_request) Successful in 5s
CI / ci (pull_request) Successful in 5m35s
# Conflicts:
#	CHANGELOG.md
#	app/build.gradle.kts
2026-06-21 13:04:18 +02:00
9fb3c780f1 Merge pull request 'build,docs: on-device release verification gate (releaseTest build)' (#20) from chore/release-verification-process into main
All checks were successful
Release — F-Droid repo + Gitea release / detect (push) Successful in 4s
Release — F-Droid repo + Gitea release / release (push) Has been skipped
Reviewed-on: #20
2026-06-21 11:01:29 +00:00
8a80478555 docs(architecture): describe merge-driven release flow
All checks were successful
Translations / check (pull_request) Successful in 6s
CI / ci (pull_request) Successful in 9m14s
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 12:33:55 +02:00
df203c0b6b ci: release on merge-to-main; consolidate pipeline triggers
All checks were successful
Translations / check (pull_request) Successful in 5s
CI / ci (pull_request) Successful in 5m55s
Reworks the pipeline so a change is built once and a release is driven by the
merge, not a manual tag push.

- ci.yaml now runs on pull_request (one gate per PR) instead of every branch
  push, so there's no CI-on-push + CI-on-merge double run. A single `ci` job
  with a docs-only fast-path keeps the required "CI" check always reporting
  (docs/metadata-only PRs skip the Android build but still go green).
- release.yaml triggers on push to main. A cheap `detect` job reads versionName
  from build.gradle; only when no tag for it exists does the `release` job run:
  tests on the merged commit, build + sign, publish to F-Droid, then create the
  vX.Y.Z tag + Gitea release via the API (target_commitish = the merged sha).
  The tag is now an OUTPUT of a successful release, not its trigger — a failure
  before publish leaves no tag, so re-running safely retries. No more separate
  tag-triggered run or duplicate ci job.
- The committed versionName/versionCode are now the source of truth (pipeline
  pins versionCode from versionName); updated the build.gradle comment.
- translations.yaml switched to pull_request (same path filter).
- docs/RELEASING.md: release-by-merge flow, no manual git tag.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 12:32:56 +02:00
0c95479051 build,docs: add on-device release verification gate
Adds a mandatory pre-tag step to the release process: build the R8-shrunk
release candidate and smoke-test it on a real device, including a first-run /
permission-not-granted state. The v2.7.0 launch crash (calendar observer
registered before the permission gate) reached users because it only manifests
in the minified release build on a device without the permission already
granted — the debug build and an already-permissioned phone both hid it.

- New `releaseTest` build type: same R8 shrinking + obfuscation as `release`,
  but debug-signed with a `.releasetest` applicationId suffix so it installs
  alongside the production and debug apps. Never published; CI only ever builds
  the real `release` variant from the tag.
- scripts/verify-release.sh: builds + installs `releaseTest` and resets it to a
  first-run state, with an on-device checklist.
- docs/RELEASING.md: formalize the release/vX.Y.Z branch flow and the on-device
  verification gate before tagging.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 12:27:30 +02:00
221313178f release: renumber crash-report release to v2.7.2
All checks were successful
CI / ci (push) Successful in 9m6s
v2.7.1 is taken by the launch-crash fix (calendar observer registered before
the permission gate). Bump this held crash-reporting release to 2.7.2.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 12:01:07 +02:00
297a2350e3 Merge pull request 'fix(calendar): don't register calendar observer before permission granted' (#19) from fix/calendar-observer-permission-crash into main
All checks were successful
CI / ci (push) Successful in 4m8s
Release — F-Droid repo + Gitea release / ci (push) Successful in 2m29s
Release — F-Droid repo + Gitea release / gitea-release (push) Successful in 5s
Release — F-Droid repo + Gitea release / build-and-deploy (push) Successful in 5m43s
Reviewed-on: #19
v2.7.1
2026-06-21 09:59:32 +00:00
997ee44792 fix(calendar): don't register calendar observer before permission granted
All checks were successful
CI / ci (push) Successful in 4m15s
The repository registers a ContentObserver on the calendar provider eagerly
in its init block, and an activity-scoped SettingsViewModel (which drives the
theme) injects that repository — so the @Singleton is constructed at launch,
above RootScreen's permission gate. On newer Android, registering an observer
on a provider you lack permission for throws SecurityException instead of
silently no-op'ing, so the app crashed instantly on every launch whenever
calendar access wasn't granted (fresh install or revoked permission), before
the permission screen could ever appear.

Guard the registration behind a calendar-permission check and re-attach the
observer lazily on the first calendars()/instances() read, which runs once the
gate opens and screens subscribe. Access to the observer collections is now
synchronized since registration can happen on the main thread (repo init) or
the IO dispatcher (query re-attach).

Verified on-device: permission-denied launch shows the permission screen
instead of crashing; granting it proceeds to the calendar with live updates.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 11:52:28 +02:00
81baadfaf3 Merge pull request 'fix(renovate): run renovate image directly instead of docker-wrapping action' (#11) from fix/renovate-action-pin into main
All checks were successful
CI / ci (push) Successful in 11m18s
2026-06-19 09:16:23 +00:00
35022267dc fix(renovate): run renovate image directly instead of docker-wrapping action
All checks were successful
CI / ci (push) Successful in 1m52s
renovatebot/github-action is a Node wrapper that shells out to
`docker run ghcr.io/renovatebot/renovate`, requiring a Docker CLI + socket
inside the job. The Gitea runner executes the job in a plain node:22 container
with neither, so it died on "Unable to locate executable file: docker".

Run the renovate image as the job container and invoke `renovate` directly —
drops the docker-in-docker requirement. Full tag pinned; Renovate's
github-actions manager keeps container.image bumped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 10:08:08 +02:00
588e024036 Merge pull request 'fix(renovate): pin action to v46.1.15' (#10) from fix/renovate-action-pin into main
All checks were successful
CI / ci (push) Successful in 1m45s
2026-06-18 20:34:59 +00:00
eeef089e4a fix(renovate): pin action to a real tag (v46.1.15)
All checks were successful
CI / ci (push) Successful in 1m31s
renovatebot/github-action ships only full semver tags; @v40 was an
invalid ref and the dispatched run failed to resolve it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 22:33:15 +02:00
9023899ddb Merge pull request 'ci(renovate): self-hosted Renovate config + weekly workflow' (#8) from feat/renovate into main
All checks were successful
CI / ci (push) Successful in 8m43s
2026-06-18 15:17:47 +00:00
5ab3344f8c release: prepare v2.7.1 — privacy-respecting crash reporting
All checks were successful
CI / ci (push) Successful in 5m16s
Bump committed versionCode/versionName to 2.7.1 (20701) and move the
crash-reporting entry under a 2.7.1 CHANGELOG heading. The tag remains the
source of truth; CI derives the published version from it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 17:14:10 +02:00
2431abe912 fix(crash): keep event/calendar content out of exception messages
Audit of our own throw sites, since exception messages land verbatim in
the stack trace a crash report carries. Redacts the three that could hold
user content; the rest only carry numeric ids/timestamps (metadata, kept
for debugging):

- create-local-calendar: drop the user-typed calendar name.
- toContentValues unsupported-type: log the value's type, never the value
  (a cell can be an event title/description/location).
- ics export open-failure: log only the Uri scheme, not the full Uri
  (which can embed the user's chosen filename).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 17:13:26 +02:00
2f153fef56 ci(renovate): self-hosted Renovate config + weekly workflow
All checks were successful
CI / ci (push) Successful in 1m31s
renovate.json5 (config:recommended + semantic commits, no automerge,
dependency dashboard; material3 stays on its 1.5-alpha pin in an
isolated PR; test deps grouped; github-actions manager watches
.gitea/workflows). Cadence owned by .gitea/workflows/renovate.yml
(Mondays 05:00 UTC + manual dispatch), self-hosted via
renovatebot/github-action, scoped to makiolaj/calendula.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 17:07:46 +02:00
701077f25b feat(crash): privacy-respecting crash reporting via Gitea issue
Capture uncaught exceptions on-device and let the user submit them, by
hand, as a Gitea issue — no network access, no auto-upload (the app holds
no INTERNET permission). Closes prod-readiness item 10; the issue
templates also close item 7.

- CrashReporter: uncaught-exception handler installed first in
  CalendulaApp.onCreate so startup crashes are caught too. Persists an
  allowlist-only report (app/Android/device version, locale, time, stack
  trace — nothing else) to filesDir/crash, then chains to the previous
  handler so the process still dies normally. Crash-loop detection +
  markHealthy reset.
- buildCrashReport is pure/testable; CrashReportBuilderTest asserts the
  header is exactly the allowlisted lines (guards against PII creep).
- Surfacing: next-launch dialog showing the full report verbatim (the
  privacy backstop) with a dismissed-marker so it doesn't nag; a Settings
  "Report a problem" row; and a minimal standalone CrashReportActivity
  that MainActivity routes to on a startup crash-loop, kept clear of the
  Hilt graph / DataStore theme.
- submitCrashReport copies the report to the clipboard and opens the
  prefilled Gitea issues/new URL (long traces fall back to paste).
- .gitea/ISSUE_TEMPLATE: crash_report, bug_report, feature_request.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 16:54:35 +02:00
290a905f8b Merge pull request 'release: v2.7.0 — ICS export & import' (#7) from release/v2.7.0 into main
All checks were successful
Translations / check (push) Successful in 6s
CI / ci (push) Successful in 9m40s
2026-06-18 14:26:53 +00:00
d20d446cbe release: cut v2.7.0 — ICS export & import (.ics share, backup, open/receive)
All checks were successful
CI / ci (push) Successful in 5m48s
Release — F-Droid repo + Gitea release / ci (push) Successful in 7m40s
Release — F-Droid repo + Gitea release / gitea-release (push) Successful in 5s
Release — F-Droid repo + Gitea release / build-and-deploy (push) Successful in 5m44s
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
v2.7.0
2026-06-18 16:24:35 +02:00
6e14d5964b fix(release): keep Room DB impls so R8 doesn't crash startup
The minified release build crashed on every launch before any UI:

  Unable to get provider androidx.startup.InitializationProvider:
    Failed to create an instance of androidx.work.impl.WorkDatabase

The home-screen widgets use Glance, which pulls in WorkManager and its
transitive Room database (room-runtime 2.2.5). Room 2.2.5's bundled keep
rule is `-keep class * extends androidx.room.RoomDatabase` — it keeps the
class but not its constructor. Under R8 full mode (AGP 9) the generated
WorkDatabase_Impl was reduced to a non-instantiable class, so Room's
reflective newInstance() threw InstantiationException at startup.

Add `-keep class * extends androidx.room.RoomDatabase { *; }` so the
generated *_Impl classes keep their constructors. Verified against the
rebuilt release APK: WorkDatabase_Impl is now PUBLIC FINAL with its
<init> present.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 16:15:08 +02:00
3dfc96718c feat(ics): import UI — open/receive .ics, 1-vs-many routing
Completes v2.7 Branch 2. Wires the import core into the app:

- Manifest ACTION_VIEW/SEND for text/calendar; MainActivity parses the
  incoming Uri (content/file only, so calendula:// deep-links don't match)
  and routes it through RootScreen → CalendarHost like the other one-shot
  intents.
- ImportViewModel reads + parses the file and routes by count: one event →
  the prefilled create form for review (EventEditViewModel.openImported,
  which freezes the reminder default so the file's reminders win); many →
  ImportScreen with a writable-calendar picker, then a bulk import (UID
  dedup) and a result summary.
- ImportScreen also surfaces parser warnings (skipped recurrence overrides,
  ignored attendees, unknown-timezone fallback). Strings EN+DE.

Package is ui.imports (not ui.import — Java keyword). lint + test +
assembleDebug green. No v2.7 tag until on-device review.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 15:20:29 +02:00
e1c2e9f2e5 feat(ics): import core — parser, dedup-aware bulk import, form prefill
v2.7 Branch 2 (core, no UI yet). The read side of the .ics engine:

- domain/ics: IcsParser (inverse of IcsWriter) — unfold/unescape/param
  parsing, VALUE=DATE / UTC-Z / TZID date handling resolved against the OS
  tz db, VEVENT walk → ParsedIcsEvent + typed warnings. Liberal-in/
  strict-out: a malformed VEVENT is skipped, RECURRENCE-ID overrides /
  attendees / unresolved TZIDs are reported, not silently dropped.
- Promoted parseRfc2445DurationMillis into domain/ics (shared by writer-
  side mapper and parser); IcsDuration + test.
- Datasource existingUids()/insertImportedEvent(); repository
  importEvents() with UID dedup (skip known UIDs → idempotent restore) →
  IcsImportSummary. IcsImporter reads a Uri's text.
- ParsedIcsEvent.toEventForm() for the single-event "open into the create
  form" path.

Parser round-trips against IcsWriter; dedup + form-adapter unit-tested.
Intent filter, routing and import UI land in the next commit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 14:59:32 +02:00
90b219bdad fix(views): stop single-day all-day events leaking into the next day
All-day events live at UTC midnights with an exclusive end, but coversDay
sliced each day in the device timezone. East of UTC the exclusive end
landed a few hours into the next local day, so a one-day all-day event
(e.g. a birthday) rendered on two days in the day/week/month views — while
the detail and edit screens, which work in UTC, showed it correctly.

Compare all-day coverage in UTC and step the exclusive end back to the
last covered day, mirroring the detail/edit views.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 14:48:34 +02:00
233a9b03a3 Merge feat/ics-export into release/v2.7.0
v2.7 Branch 1 of 2: .ics export — single-event share + whole-calendar backup of local calendars. Import (feat/ics-import) lands next in the same release.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 14:45:59 +02:00
0b683d374f feat(ics): export — share single event + back up local calendars as .ics
Branch 1 of 2 for v2.7 (the .ics topic). Adds the write side of a
hand-rolled RFC 5545 engine (zero deps, stays on kotlinx-datetime):

- domain/ics: IcsText (escape + 75-octet folding), IcsEvent model,
  IcsWriter.writeCalendar. Timezone rule: all-day VALUE=DATE, one-off
  timed UTC Z, recurring timed TZID-labelled from EVENT_TIMEZONE (no
  VTIMEZONE — import resolves TZID against the OS tz db).
- Single-event share from the detail screen (FileProvider + ACTION_SEND).
- Whole-calendar backup of the writable local calendars to a SAF file
  (Settings -> Calendars -> Export as .ics), one combined VCALENDAR.
- insertEvent now writes Events.UID_2445; legacy rows fall back to a
  stable synthesised UID at export time so a later restore won't dupe.
- EXDATE / RECURRENCE-ID overrides are deliberately skipped this pass
  (documented v1 limit; import will skip them too).

Engine + mapper unit-tested. Import (Branch 2, feat/ics-import) ships in
the same v2.7 release; no tag until both land + on-device review.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 14:27:53 +02:00
64d0a89b28 release: cut v2.6.0 — working in-app language picker + system per-app language
All checks were successful
Release — F-Droid repo + Gitea release / ci (push) Successful in 7m43s
Release — F-Droid repo + Gitea release / gitea-release (push) Successful in 5s
CI / ci (push) Successful in 9m33s
Release — F-Droid repo + Gitea release / build-and-deploy (push) Successful in 7m2s
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
v2.6.0
2026-06-18 12:38:58 +02:00
7285e274df Merge pull request 'feat(i18n): data-driven language picker + Weblate translation guard' (#5) from feat/translations into main
All checks were successful
Translations / check (push) Successful in 4s
CI / ci (push) Successful in 1m43s
2026-06-18 08:41:46 +00:00
788ca3906e Merge remote-tracking branch 'origin/main' into worktree-feat+translations
All checks were successful
Translations / check (push) Successful in 4s
CI / ci (push) Successful in 5m11s
2026-06-18 10:29:00 +02:00
bab6fd175a fix(i18n): make the language picker actually apply on device
The in-app language picker silently did nothing: AppCompatDelegate.set
ApplicationLocales only syncs to the system from an AppCompatActivity, but
MainActivity was a plain ComponentActivity (with a platform theme). Switch
MainActivity to AppCompatActivity and base Theme.Calendula on
Theme.AppCompat.DayNight.NoActionBar.

Changing the locale recreates the activity; set android:windowBackground to a
DayNight colour matching the Compose background (light #FBFCFE / dark #101316)
so the recreation no longer flashes a contrasting backdrop.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 10:28:13 +02:00
3d5cc55ef1 Merge pull request 'feat(reminders): configurable all-day reminder fire time' (#6) from feat/default-reminders into main
All checks were successful
CI / ci (push) Successful in 10m19s
2026-06-18 07:58:47 +00:00
111b3782b0 feat(reminders): configurable all-day reminder fire time
All checks were successful
CI / ci (push) Successful in 3m37s
All-day events live at UTC midnight, so a raw "1 day before" reminder
fires at an off hour (02:00 local in CEST) rather than the morning. Add a
global "all-day reminder time" setting (default 09:00) and encode it into
the provider MINUTES offset so the reminder lands at the chosen wall-clock
time the day before instead.

- AllDayReminderEncoding: pure to/from provider-minutes helpers, keeping
  the form/UI/diff in whole-day "semantic" minutes and converting only at
  the Reminders read/write boundary (insertEvent, reconcileReminders,
  EventDetailMapper). Covers DST, negative offsets, and pre-existing rows.
- SettingsPrefs.allDayReminderTimeMinutes (default 540) threaded from the
  repository into the data-source write paths.
- Settings: a time-picker row, plus a shared TimePickerAlert lifted from
  the event editor.
- Fix the time picker's 12/24-hour detection: honour an explicit system
  override, else fall back to the device locale rather than the app's
  per-app language, so it matches the rest of the device.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 09:54:41 +02:00
cf380b6eab ci(i18n): translation parity guard + allow partial translations
Some checks failed
CI / ci (push) Failing after 17m45s
Translations / check (push) Successful in 7s
Add scripts/check_translations.py and a lightweight Translations workflow
that runs it (no Android SDK needed) so Weblate PRs get fast feedback. The
script fails on stale keys (present in a translation but not the base) and on
translating translatable="false" entries; missing keys are reported as
coverage only.

Downgrade lint's MissingTranslation to informational: partial community
translations are expected and fall back to the English base at runtime.
Stale/extra keys (ExtraTranslation) remain fatal in lintDebug.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 08:43:06 +02:00
9177a926df feat(i18n): data-driven language picker + locale config
Make the supported-language list a single source of truth so community
translations show up with no code change: add res/xml/locales_config.xml
(en, de) and reference it via android:localeConfig, which also surfaces the
per-app language entry in Android 13+ system settings.

Rewrite AppLanguage to parse locales_config.xml for the supported BCP-47
tags and expose currentTag/apply/displayName (autonyms), dropping the
hardcoded LanguagePref enum; the Settings picker is now built from that list.
Remove the now-unused settings_language_german/english strings.

Adding a language is now: drop in values-<tag>/strings.xml and add one
<locale> line.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 23:31:54 +02:00
5e6defd4c7 release: cut v2.5.0 — home-screen widgets, agenda, jump-to-date, quick actions
All checks were successful
CI / ci (push) Successful in 12m38s
Release — F-Droid repo + Gitea release / ci (push) Successful in 2m19s
Release — F-Droid repo + Gitea release / build-and-deploy (push) Successful in 10m1s
Release — F-Droid repo + Gitea release / gitea-release (push) Successful in 8s
Bundles the unreleased Tier 2/3 work into one release:

- Home-screen widgets (Glance): an "Upcoming" agenda widget and a month-grid
  widget, both reusing the in-app grouping/layout (groupAgendaDays,
  layoutMonthWeeks) via a Hilt WidgetEntryPoint, honouring hidden-calendar
  filters and refreshing on PROVIDER_CHANGED / date rollover.
- App shortcut: launcher long-press "New event", routed through the shared
  WidgetNavRequest.Create channel into the create-event form.
- Agenda view and jump-to-date (already merged via #3/#4) are documented here
  as part of the shipped version.

Bumps versionCode 20500 / versionName 2.5.0, moves the CHANGELOG Unreleased
section under [2.5.0], updates ROADMAP/STATE, and adds EN+DE strings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
v2.5.0
2026-06-17 15:33:58 +02:00
6e7ae3e60d Merge pull request 'feat(agenda): Agenda view — upcoming events grouped by day' (#4) from feat/agenda-view into main
All checks were successful
CI / ci (push) Successful in 6m45s
2026-06-17 07:45:04 +00:00
b0b30eef91 feat(agenda): add Agenda view — upcoming events grouped by day
All checks were successful
CI / ci (push) Successful in 6m24s
The fourth top-level view, alongside Month/Week/Day. A forward-looking
LazyColumn of upcoming events grouped under sticky day headers, reusing
the v2.3 grouped-list language (GroupedRow cards, color-rail leading).

- AgendaViewModel loads a 60-day forward window from the anchor day
  (today by default; goToToday/goToDate drive the FAB + drawer jump),
  groups instances by local day (ongoing/multi-day clamped to the
  anchor), sorts all-day-first then by start.
- AgendaScreen: same drawer + scaffold + view-switcher + FAB shell as
  Day; sticky "Today · …"/"Tomorrow · …" headers, event rows with
  time·location, plus empty/failure/loading states.
- Wired into CalendarView (ViewAgenda icon), IMPLEMENTED_VIEWS, and
  CalendarHost; strings added (EN + DE).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 09:41:36 +02:00
8b25c9be39 Merge pull request 'feat(nav): jump-to-date action in the navigation drawer' (#3) from feat/jump-to-date into main
All checks were successful
CI / ci (push) Successful in 6m31s
2026-06-17 07:25:46 +00:00
2943f3945d feat(nav): jump-to-date action in the navigation drawer
All checks were successful
CI / ci (push) Successful in 6m17s
Add a "Jump to date" row to the drawer (under the View switcher) that
opens an M3 date picker and navigates the active view to the chosen day,
sliding in from the correct side. Wired across Month/Week/Day, each
seeding the picker with its visible anchor (day / week-start / 1st-of-month).

Extract the form's private date-picker into a shared
ui/common/CalendarDatePickerDialog so the event form and the drawer share
one picker; add goToDate() to the Month and Week view models.

Reprioritises the roadmap: jump-to-date is now next; duplicate-event drops
to the bottom as low-importance.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 09:24:49 +02:00
b62f097392 release: cut v2.4.0 — per-event colors
All checks were successful
CI / ci (push) Successful in 9m20s
Release — F-Droid repo + Gitea release / ci (push) Successful in 2m3s
Release — F-Droid repo + Gitea release / build-and-deploy (push) Successful in 9m22s
Release — F-Droid repo + Gitea release / gitea-release (push) Successful in 7s
Optional per-event color in the event form. The read/render path already
resolved EVENT_COLOR with a calendar fallback; this adds the write side and
the picker.

- Palette-backed calendars (Google, some CalDAV) pick from the account's
  Colors (TYPE_EVENT) and write EVENT_COLOR_KEY, so the color round-trips
  through sync; local calendars write a raw EVENT_COLOR from the shared
  CALENDAR_COLOR_PALETTE. Never writes a raw color to a palette calendar.
- Swatch row + palette extracted to ui/common/ColorSwatchRow.kt (shared with
  the calendar editor). Switching calendars resets the choice (keys are
  account-scoped); a "Reset" action returns to the calendar color.
- New "Allow colors on unsupported calendars" setting (off by default)
  extends the raw path to no-palette synced calendars, with an honest
  "may not survive sync" warning on the picker and in Settings.
- Color flows through insert / dirty-checked update / occurrence-exception;
  mapper, form, and repository tests added.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
v2.4.0
2026-06-17 08:55:16 +02:00
210ddff8d8 release: cut v2.3.0 — Material 3 grouped-list redesign of Settings, calendars & drawer
All checks were successful
CI / ci (push) Successful in 8m2s
Release — F-Droid repo + Gitea release / ci (push) Successful in 2m1s
Release — F-Droid repo + Gitea release / build-and-deploy (push) Successful in 8m44s
Release — F-Droid repo + Gitea release / gitea-release (push) Successful in 8s
One shared Material 3 grouped-list blueprint, modelled on the ReFra gallery app
and extracted to ui/common/GroupedList.kt: CollapsingScaffold (a LargeTopAppBar
whose large title collapses into the bar on scroll) and GroupedRow
(Position-based corner grouping so a run of rows reads as one rounded card, with
press-animated corners and selected/minHeight knobs).

Settings: restructured into a category hub (About card on top, version mark at
the foot) with sliding sub-pages for Appearance, the new-event form and
Notifications. Theme, week-start and language pickers migrated from DropdownMenu
to OptionCard dialogs; token-based icon chips. New ic_gitea.xml (Simple Icons,
verbatim path) for the About "Source" button; en+de strings.

Calendar manager: same collapsing scaffold + grouped rows; shared
CalendarColorChip (neutral chip with a pastelised calendar glyph) replaces the
bright colour swatch.

Navigation drawer: branded header, grouped View switcher (active view
highlighted via secondaryContainer), filter list restyled to grouped rows with a
trailing checkbox; the whole drawer now scrolls as one.

Cards use surfaceContainerHigh for readable contrast against surface. Version
bumped to 2.3.0 / 20300. UI-only; unit tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
v2.3.0
2026-06-16 11:44:10 +02:00
e194da3766 release: cut v2.2.0 — tap-to-create + local calendar management
All checks were successful
CI / ci (push) Successful in 8m53s
Release — F-Droid repo + Gitea release / ci (push) Successful in 1m59s
Release — F-Droid repo + Gitea release / build-and-deploy (push) Successful in 8m57s
Release — F-Droid repo + Gitea release / gitea-release (push) Successful in 8s
Day/week: tap an empty slot to open the create form prefilled with that
day and the tapped hour (snapped to the hour, 1 h long). Threaded a start
time through CalendarHost → EventEditScreen → openNew; the FAB keeps its
default.

Local calendars: a full-screen editor from Settings → Calendars to
create/rename/recolor/delete device-only calendars (ACCOUNT_TYPE_LOCAL,
sync-adapter insert) with name, pastel-previewed colour, and a description
(stored in CAL_SYNC1). Synced calendars are listed read-only grouped by
account, each with a "manage in source app" deep-link resolved from the
account's own authenticator (DAVx5/ICSx5/…), plus an add-account shortcut;
a <queries> block makes the source apps launchable. Extracted a shared
InlineTextField into ui.common so the event form and calendar editor share
one borderless input style.

Tests: repository delegation + write-failure, mapper isLocal/description,
fake data source extended. Version bumped to 2.2.0 / 20200.

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