119 Commits

Author SHA1 Message Date
54b059e744 chore(release): prepare v2.10.0
All checks were successful
Translations / check (pull_request) Successful in 5s
CI / ci (pull_request) Successful in 6m6s
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 14:31:24 +02:00
bf10fee19f Merge remote-tracking branch 'origin/release/v2.10.0' into feat/disable-calendar
All checks were successful
Translations / check (pull_request) Successful in 5s
CI / ci (pull_request) Successful in 8m57s
# Conflicts:
#	CHANGELOG.md
2026-06-25 14:23:07 +02:00
450fc7f7af Merge remote-tracking branch 'origin/main' into feat/disable-calendar 2026-06-25 14:05:45 +02:00
be6d10d126 feat(calendars): add app-side enable/disable for calendars
Introduce a second, heavier visibility level above the per-view hide
filter. A disabled calendar is removed from the app's surfaces entirely —
its events drop out of all views and search, and it disappears from the
drawer filter list, the event-form calendar picker and the import target
picker. It stays listed only in Settings → Calendars, where a per-row
switch toggles it, so it can always be brought back.

- CalendarPrefs: disabledCalendarIds set + setter, mirroring the hidden
  set (DataStore comma-separated string); never touches the system
  VISIBLE/SYNC_EVENTS flags, so it's app-local and reversible.
- CalendarRepositoryImpl: instances()/searchEvents() exclude
  calendarId ∈ (hidden ∪ disabled). distinctUntilChanged() on instances
  collapses the transient duplicate emission both DataStore-derived sets
  produce when either is toggled.
- FilterViewModel: drop disabled calendars from the drawer filter list.
- EventEditViewModel: exclude disabled from writableCalendars; a
  last-used preselect on a now-disabled calendar falls back to the first
  remaining writable one.
- ImportViewModel: exclude disabled from the import target list.
- CalendarsScreen/ViewModel: per-row enable/disable Switch on both the
  local and synced groups; disabled rows render dimmed (new GroupedRow
  `dimmed` flag) while keeping the toggle live.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 13:55:10 +02:00
a9c67a5827 feat(about): add Ko-fi support link
Add an optional way for users to support development:

- About card: tonal "Support development" button opening ko-fi.com
  via ACTION_VIEW (no INTERNET permission), EN + DE strings
- F-Droid metadata: Donate field in self-hosted and official drafts
- README: short Support section with the Ko-fi link

No perks/rewards attached — kept as a plain donation link.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 12:39:49 +02:00
024deadf78 chore(release): prepare v2.9.0
All checks were successful
Translations / check (pull_request) Successful in 5s
CI / ci (pull_request) Successful in 9m5s
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 09:56:54 +02:00
c6d15d0118 feat(ui): consistent motion polish, predictive back & reduced-motion support
Centralise the app's motion vocabulary in CalendarTransitions.kt and route
every surface through it so animation is consistent app-wide:

- Shared expand/collapse, list-item and fade-through helpers; the event-edit
  expand pattern is now the shared one (no duplicate).
- Settings reminder-override rows and the reminder Custom field now expand/
  collapse instead of bare-fading.
- Search and agenda rows animate (fade/relocate) via animateItem.
- Month/week/day slide and the onboarding gates honour the new helpers.
- View switching (month/week/day/agenda) now fades through instead of
  snapping — lateral navigation per M3, while in-view paging keeps its slide.

Add full predictive-back support:

- enableOnBackInvokedCallback in the manifest.
- New Modifier.predictiveBack(onBack) drives the standard preview transform
  (scale/shift/round) following the back gesture; applied to detail, edit,
  search, settings (+ sub-screens & calendar manager via CollapsingScaffold),
  the calendar editor and import — each keeping its existing back semantics.

Reduced-motion guardrail throughout: rememberReduceMotion() (reads the OS
"remove animations" setting, which Compose ignores by default) collapses
spatial motion to a quick fade and skips the back preview.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 09:19:35 +02:00
119a8afe8e feat(nav): interactive month widget + history-retracing back
All checks were successful
Translations / check (pull_request) Successful in 28s
CI / ci (pull_request) Successful in 4m18s
On-device review follow-ups:

- Month widget grid is now tappable. Day numbers open that day and event
  bars open the event's detail, both rooted in the month view so back
  returns to the grid. Previously only the prev/next/today header
  controls responded — the grid cells were never clickable.

- Pill/drawer view switches now build a visit history instead of
  collapsing to the default view. Back retraces the views you moved
  through (a not-yet-visited view is pushed; revisiting one collapses the
  loop back to it), down to the default, then exits. Widget launches
  still reset to their own view context.

Refs #1, #2 (reported by @devinside).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 08:17:23 +02:00
b537e143f8 feat(nav): default view setting + widget-aware back stack
Replace CalendarHost's single global view slot with a top-level view
back stack rooted at a user-configurable default view. A lateral move
(pill/drawer/widget) replaces the non-home top; a date tap drills the
day view on top; a base-level BackHandler pops one level until only the
home view remains, then the system exits.

Widgets now carry their source view (EXTRA_SOURCE_VIEW) so a launch
roots the stack in that widget's view: backing out of a day/event opened
from the agenda widget returns to Agenda, and from the month widget to
Month, instead of always landing on Week. Reminder taps keep the
separate detail-key channel and leave the base view untouched.

Add a Default view setting (Settings -> Appearance) backing the stack's
home view; defaults to Week so existing users see no change.

Resolves the two Codeberg reports from @devinside:
- #1 [FR] Option to set default view
- #2 [Bug] Widget UX improvement

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 08:07:00 +02:00
3d8e3ca69e chore(release): prepare v2.8.0
All checks were successful
Translations / check (pull_request) Successful in 5s
CI / ci (pull_request) Successful in 6m5s
Bump versionName to 2.8.0 (versionCode 20800) — the merge of this to main
is what cuts the release. Move the accumulated 2.8.0 work out of
[Unreleased] into a dated CHANGELOG section and regenerate the F-Droid
per-version changelog.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 11:42:56 +02:00
3b5c0be765 Merge pull request 'feat: full-text event search' (!33) from feat/event-search into release/v2.8.0
Reviewed-on: #33
2026-06-23 09:27:51 +00:00
013efef29e fix(search): clear on reopen and centre messages above the keyboard
All checks were successful
Translations / check (pull_request) Successful in 4s
CI / ci (pull_request) Successful in 4m14s
The search ViewModel is activity-scoped and outlives the overlay, so a reopened
search showed the previous query/results — reset the query when the screen
re-enters (peeking a result keeps it, as the screen stays composed under the
detail). Add imePadding so the idle/empty message re-centres in the area above
the keyboard instead of staying centred on the full page behind it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 11:05:26 +02:00
5457a34282 feat(search): show a recurring result's next occurrence
A recurring master's DTSTART is the series start, which for long-running series
reads as an old date and sorts into the past. For recurring hits (non-empty
RRULE/RDATE), resolve the occurrence nearest to now via the Instances provider —
the soonest upcoming within ~2 years, else the most recent past — and display
and sort by that. Falls back to the series start when no occurrence lies in the
window.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 10:51:28 +02:00
82e93fda4c feat(search): full-text event search from the top bar
A magnifier in each calendar screen's top bar opens a search overlay: type a
query and matching events (title, location or description) appear, nearest-to-
today first; tapping a result opens its detail.

- Data: query the Events table directly with a LIKE selection on title /
  description / location (wildcards escaped), so search is unbounded in time and
  filtered provider-side. New SearchProjection + toSearchResult mapper reuse the
  .ics export's DURATION handling for recurring masters. Hidden calendars are
  filtered out, mirroring instances().
- SearchViewModel debounces the query (250 ms), needs >= 2 chars, and orders
  results upcoming-ascending then past-descending.
- SearchScreen: autofocused inline field in the top bar, GroupedRow results
  reusing the agenda row style, idle/empty states. Hosted as a CalendarHost
  overlay below detail/edit so a tapped result's detail draws on top.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 10:42:37 +02:00
871cc34cab Merge pull request 'feat: 'New event' Quick Settings tile' (!32) from feat/qs-tile-new-event into release/v2.8.0
Reviewed-on: #32
2026-06-23 08:18:09 +00:00
5b99da32b0 fix(qs): silence StartActivityAndCollapseDeprecated lint error
All checks were successful
Translations / check (pull_request) Successful in 4s
CI / ci (pull_request) Successful in 4m15s
lintDebug aborts on the deprecated startActivityAndCollapse(Intent) overload in
the pre-34 branch, even though that overload is the only one available below
UpsideDownCake and is reached only there. Suppress the lint issue (and the
compiler deprecation) at the function level since the call is intentional and
version-gated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 10:13:33 +02:00
b8a7191fbe fix(nav): bring external new-event/open-date to the front
Some checks failed
Translations / check (pull_request) Successful in 4s
CI / ci (pull_request) Failing after 3m36s
The create form and Settings/calendar-manager are sibling overlays in one Box,
with Settings drawn above the form. An external 'new event' request (QS tile,
launcher shortcut, widget) set the create state correctly but rendered the form
underneath an open Settings, forcing the user to back out first.

Dismiss the covering overlays (Settings, calendar manager, detail/edit, import)
when handling a Create or OpenDate nav request, so the requested destination is
revealed on top. Fixes the same latent bug for the shortcut and widget.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 09:59:44 +02:00
6b0eb48056 Merge pull request 'feat: current-time line in day and week views' (!31) from feat/now-line into release/v2.8.0
Reviewed-on: #31
2026-06-23 07:59:12 +00:00
572a4734ea feat(qs): add a 'New event' Quick Settings tile
A stateless TileService that opens the create-event form on today — the same
action as the launcher 'New event' shortcut and the agenda widget's +. Reuses
MainActivity.openCreateIntent; wrapped in unlockAndRun and using the API 34+
startActivityAndCollapse(PendingIntent) form (deprecated Intent form below 34).
No new permission (BIND_QUICK_SETTINGS_TILE is system-side).

Discoverability: Settings → New event form gains an 'Add Quick Settings tile'
row that fires StatusBarManager.requestAddTileService (API 33+); on older
versions the tile is still addable manually from the system QS editor.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 09:52:24 +02:00
7f8a9069c0 feat(views): show a current-time line in day and week views
All checks were successful
CI / ci (pull_request) Successful in 4m21s
A thin primary-coloured indicator (leading dot + line) marks the current
time across today's column in the day and week timelines, positioned on the
same HOUR_HEIGHT scale as the event blocks so it lines up with the grid.

Shared ui/common/NowLine.kt ticks once a minute, re-aligning to each minute
boundary to avoid drift, and only mounts on today's column so a single
coroutine runs. Renders nothing when today isn't in view.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 09:36:40 +02:00
9839f9cd38 feat(edit): pick a location from the contact picker
All checks were successful
Translations / check (pull_request) Successful in 4s
CI / ci (pull_request) Successful in 4m19s
Reuse the attendee contact-picker mechanism for the location field: a
Contacts button beside the location input opens the system picker scoped
to postal-address rows, and the chosen contact's formatted address is
dropped into the field (multi-line addresses collapsed to one line).

Same no-permission guarantee as the guest picker — ACTION_PICK grants
temporary read access, so no READ_CONTACTS is required.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 09:22:51 +02:00
c0e27133b4 Merge pull request 'feat: editable event attendees with contact picker' (!29) from feat/attendee-editing into release/v2.8.0
Reviewed-on: #29
2026-06-23 07:12:28 +00:00
a4e0ec3fde feat(edit): add guests from the contact picker
All checks were successful
Translations / check (pull_request) Successful in 28s
CI / ci (pull_request) Successful in 9m19s
Close the name-on-manual-add gap. The add-guest row gains a "from
contacts" button that launches the system contact picker (ACTION_PICK on
ContactsContract Email URI); the picked email row is queried for
Email.ADDRESS + DISPLAY_NAME, so a chosen guest gets both email and name
in one tap. No READ_CONTACTS — the result Intent grants temporary read
access to just that row, so Calendula stays no-permission/no-network. The
inline email field remains the quick email-only path. Mark the roadmap
item shipped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 23:50:23 +02:00
b0f34ff18a feat(edit): make event attendees editable
Attendees were read-only (shown on the detail screen since v0.6) but the
form couldn't write them — the last read-only gap in the event model.
Add a Guests section to the create/edit form:

- Inline grouped list: each guest is a tonal card (avatar, name/email, a
  tappable Required/Optional role chip, remove); the trailing card is an
  inline email field — type an address, press Done, it commits. No dialog,
  matching the app's inline-field input idiom. Manual adds are email-only
  (names come from sync); the InlineTextField gains IME-action support.
- Reminders restyled to the same grouped-list pattern (shared
  GroupedItemCard / AddActionCard), replacing the single-card blob.

Persistence (CalendarDataSource): new guests are written as plain
RELATIONSHIP_ATTENDEE / STATUS_INVITED rows — no fabricated organizer.
On edit, a dirty-checked reconcileAttendees diffs by email: drops removed
guests, inserts new ones, updates only the required/optional flag on kept
rows (preserving response status). Organizer, resources and no-email rows
are never touched. toEditForm carries only editable guests, so attendees
now ride in the edit snapshot and an external guest change trips the
conflict check.

Per the settled invitation decision: Calendula has no INTERNET and never
sends an invitation — it only writes the rows; the backend decides
delivery. The section shows honest, calendar-aware copy ("your account
may email guests when it syncs" on synced calendars, "no one is notified"
on local).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 23:42:37 +02:00
4ad1c09f8f feat(detail): show attendee email under the name
The attendee row only used the email as a fallback when the name was
blank, so a guest with both a name and an email never showed the email.
Add it as a supporting line (bodySmall / onSurfaceVariant) beneath the
name, kept off rows whose headline already falls back to the email.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 23:00:34 +02:00
5e8cb686e9 Merge pull request 'feat(reminders): snooze + dismiss notification actions' (!28) from feat/reminders-snooze into release/v2.8.0
Reviewed-on: #28
2026-06-22 20:19:36 +00:00
37452be3bd feat(crash): file reports via the public Codeberg tracker instead of email
All checks were successful
Translations / check (pull_request) Successful in 4s
CI / ci (pull_request) Successful in 3m22s
Now that a public, writable issue tracker exists (the Codeberg mirror at
codeberg.org/jlmakiola/calendula, which the Gitea issue tab links to),
revert the email hand-off (6150ce6) back to the in-app issue-creation flow:
the crash path opens a prefilled issues/new page (with clipboard copy as the
long-report fallback) and the manual "Report a problem" path opens the issue
template chooser.

The email pivot existed only because the personal Gitea has no public issue
creation (reporters hit a login wall). Codeberg lets anyone register and
file, so that reason is gone. Still no INTERNET permission — the user submits
via the browser themselves. URLs point at Codeberg directly so the prefilled
title/body survive (a Gitea external-tracker redirect wouldn't carry query
params).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 22:03:10 +02:00
006cce96a9 feat(crash): hand off reports by email instead of the Gitea issue tracker
Filing a Gitea issue requires an account on the instance, so anonymous
reporters hit a login wall — Gitea has no anonymous issue creation. Switch
both the crash-report and manual problem-report paths to compose a
pre-addressed email via ACTION_SENDTO (mailto:), which needs no account and
preserves the existing no-INTERNET, user-sends-it-themselves model. The full
report rides in EXTRA_TEXT, so the old URL-length cap and clipboard-paste
fallback are gone (clipboard copy stays as a safety net).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 22:03:10 +02:00
7ab277b434 feat(reminders): snooze + dismiss notification actions
Add "Snooze" and "Dismiss" action buttons to reminder notifications.

The app is otherwise pure provider-broadcast (the Etar model): the calendar
provider fires EVENT_REMINDER and we post the notification, then mark the
CalendarAlerts row fired. A snoozed reminder has no provider backing — its
row is already fired — so snooze self-schedules an exact alarm to re-show
the same notification, while primary delivery is left unchanged.

- ReminderActionReceiver (not exported): SNOOZE cancels + schedules a
  re-show, DISMISS cancels, SHOW (the alarm) re-posts so it can be snoozed
  or dismissed again.
- ReminderSnoozeScheduler: setExactAndAllowWhileIdle, with an inexact
  allow-while-idle fallback if exact alarms are revoked (API 31-32).
- ReminderNotifier: two actions + cancel().
- snoozeMinutes pref (default 10) in Settings -> Notifications, OptionPicker
  presets 5/10/15/30/60.
- Manifest: USE_EXACT_ALARM + SCHEDULE_EXACT_ALARM (maxSdk 32) + receiver.
- New ic_notification_snooze/_dismiss drawables, duration plurals, en+de
  strings, snooze-pref tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 22:21:01 +02:00
5c80f99179 build(fdroid): drop AGP dependency-metadata block; release v2.7.5
All checks were successful
CI / ci (pull_request) Successful in 5m57s
F-Droid's binary scanner (the fdroiddata `check apk` job) rejects any
"extra signing block". Our release APK carried AGP's dependency-metadata
block (id 0x504b4453, ~8 KB) in the APK Signing Block. Disable it with
`dependenciesInfo { includeInApk = false; includeInBundle = false }`.

The block lives in the signing block, not the zip entries, so removing it
changes no build output — v2.7.5 is functionally identical to 2.7.4 and
still reproduces byte-for-byte. Verified locally: a releaseTest build now
carries only the v2 signature + verity padding (no 0x504b4453 block).

This was the last F-Droid blocker: v2.7.5 now clears vcsInfo (since 2.7.3),
foojay (since 2.7.4) and the dependency-metadata block. Bumps versionName
to 2.7.5 and retargets the official recipe draft at v2.7.5.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 20:48:59 +02:00
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
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
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
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
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
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
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
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
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>
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
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>
2026-06-18 12:38:58 +02: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