Commit Graph

350 Commits

Author SHA1 Message Date
1a3e4f501f fix(edit): time picker dial honours the app 24h/12h setting (#27)
The event-form (and Settings) time picker seeded is24Hour from the
system TIME_12_24 override / device locale, ignoring the app's own
TimeFormatPref. So with the app set to 24h under an English locale the
dial still showed AM/PM, while every time label (which reads
LocalUse24HourFormat) showed 24h.

Seed the picker from LocalUse24HourFormat — the app-wide clock
convention already resolved once at the root from TimeFormatPref — so the
dial matches the labels. Drops the now-unused deviceUses24HourClock
helper.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 16:05:43 +02:00
974d65f619 fix(detail): open events whose series starts before 1970 (#34)
DTSTART is stored as UTC epoch millis, so a recurring series anchored
before 1970-01-01 (common for yearly birthdays/anniversaries synced over
CalDAV) has a legitimately negative DTSTART. The detail and search
mappers dropped any row with dtstart < 0, and since the detail query
reads the series-master DTSTART (the ancient anchor), every occurrence of
such a series became un-openable — surfacing as the generic
"Something went wrong." error screen — and the events vanished from
search too.

Relax the guard to reject only an *absent* DTSTART (isNull), which is the
malformed case it was meant to catch; negative epoch millis flow through
correctly (Instant/formatting and the all-day reminder decode are all
Long-based). Add regression tests for both mappers.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 16:05:33 +02:00
2239c38ecc Merge pull request 'release: v2.13.0 — special dates, custom fonts, quick-switch, es/it translations' (!58) from release/v2.13.0 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) Successful in 11m16s
Renovate / renovate (push) Successful in 1m12s
Reviewed-on: #58
v2.13.0
2026-07-03 14:47:42 +00:00
79ad70e0c0 release: cut v2.13.0 changelog and fastlane notes
All checks were successful
Translations / check (pull_request) Successful in 4s
CI / ci (pull_request) Successful in 9m3s
Move the accumulated [Unreleased] section under [2.13.0] — 2026-07-03,
adding the entries that were still missing: Spanish + Italian community
translations (thanks kikerw and corrent via Weblate), custom fonts (#19),
configurable quick-switch cycle + drawer order (#24), and the scrollable
event-editor calendar picker (#29). Regenerate fastlane/.../changelogs/
21300.txt via scripts/sync_changelog_to_fastlane.sh.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 16:25:39 +02:00
f168021f51 release: bump version to 2.13.0 (21300)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 16:25:39 +02:00
a2b1e7792b chore(i18n): drop stale reminder_custom_with_value from es/it
The source key was removed on this branch; the Weblate translations
merged from main still carried it, tripping check_translations.py and
fatal ExtraTranslation lint.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 16:10:31 +02:00
c6ce0830b1 Merge remote-tracking branch 'origin/main' into release/v2.13.0 2026-07-03 16:09:51 +02:00
be13f97802 feat(i18n): surface Spanish and Italian in the language pickers
Community translations for es and it reached ~85% coverage (Weblate
PR #52 into main). Adding their locale entries makes them selectable in
the in-app picker and Android 13+ per-app language settings once that
PR is merged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 16:05:22 +02:00
f6d40de832 chore(i18n): strip hand-edited German strings — German returns via Weblate
Weblate owns all translation files including values-de; devs only touch
the English base. Reverts the hand-added special-dates strings and the
permission_rationale_body rewording to main's version (texts recoverable
from 1ff4115, to be re-added through the Weblate API). Keeps the removal
of the stale reminder_custom_with_value key, whose source string was
deleted on this branch (ExtraTranslation is fatal).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 16:05:22 +02:00
fdee311658 Merge pull request 'Translations update from Weblate' (!52) from weblate-bot/calendula:weblate-calendula-strings 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: #52
2026-07-03 14:04:16 +00:00
1ff41153e9 Merge pull request 'feat(contacts): mirror contact special dates into local calendars (#15)' (!57) from feat/contact-special-dates into release/v2.13.0
Reviewed-on: #57
2026-07-03 13:44:46 +00:00
49d04bcee0 Merge remote-tracking branch 'origin/release/v2.13.0' into feat/contact-special-dates
All checks were successful
Translations / check (pull_request) Successful in 26s
CI / ci (pull_request) Successful in 5m19s
# Conflicts:
#	app/src/main/java/de/jeanlucmakiola/calendula/data/prefs/SettingsPrefs.kt
#	app/src/main/java/de/jeanlucmakiola/calendula/ui/edit/EventEditScreen.kt
#	app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsScreen.kt
#	app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsViewModel.kt
#	app/src/test/java/de/jeanlucmakiola/calendula/data/prefs/SettingsPrefsTest.kt
2026-07-03 15:41:38 +02:00
5e470baf4e Merge pull request 'fix(edit): scrollable, grouped, pinned-header calendar picker (#29)' (!56) from fix/calendar-picker-scrollable into release/v2.13.0
Reviewed-on: #56
2026-07-03 13:35:48 +00:00
c8a4f90404 Merge remote-tracking branch 'origin/release/v2.13.0' into fix/calendar-picker-scrollable
All checks were successful
CI / ci (pull_request) Successful in 5m16s
2026-07-03 15:22:38 +02:00
0a89ff9d0e fix(edit): pin the picker title instead of a large collapsing header
Full-screen selection pickers opened with the LargeTopAppBar expanded, so the
title started below the bar and only settled into it after a scroll — a tall
empty header above a short option list. Give CollapsingScaffold a largeTopBar
flag and have FullScreenPicker use a pinned single-line TopAppBar, so every
picker (calendar, theme, week start, language, reminder, agenda range) shows
its title in the bar from the start. Settings and the calendar manager keep
the large collapsing header.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 15:22:18 +02:00
0718afd5a6 fix(edit): make the new-event calendar picker a scrollable grouped list
The calendar picker was an AlertDialog holding a non-scrolling Column, so
accounts with many calendars overflowed the dialog's fixed height and the
entries past ~9 were unreachable (Codeberg #29).

Replace it with the app's FullScreenPicker (collapsing scaffold + scrollable
content), rendering calendars as connected GroupedRows grouped under their
owning account — the same grouping the visibility filter and calendar manager
use — with a colour chip per row and a check on the current selection.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 23:13:27 +02:00
84e6402031 fix(contacts): serialize sync/teardown and stop retrying on lost permission
- The periodic job and the immediate 'Sync now'/foreground runs had no mutual
  exclusion, so two overlapping reconciles could each see no managed calendar
  and both create one (duplicate 'Birthdays', doubled events); likewise a
  disable-teardown racing an in-flight sync got its calendars recreated right
  after deletion. A lifecycle Mutex now makes sync() and teardown() atomic, and
  sync() re-reads the enabled flag inside the lock so a teardown always wins.

- The foreground resume trigger shared a unique work name with enable/'Sync
  now' under ExistingWorkPolicy.REPLACE, so a debounced foreground enqueue could
  cancel-and-swallow a pending enable sync (feature on, no calendars for up to a
  day). It now uses its own work name.

- doWork() mapped every exception to retry(), so a revoked calendar permission
  retried with backoff forever and never surfaced. A SecurityException now parks
  the feature in the stalled state instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 23:04:26 +02:00
7fe59b36c3 fix(contacts): keep distinct custom dates instead of collapsing them
The mirror keyed each event on (contact, type) only, and the read deduped on
that same pair from an unordered query. So a contact with two custom dates
('Wedding', 'Graduation') mirrored just one — and, the winner being
order-dependent, the single event could ping-pong between the two dates across
syncs. A Custom date's UID now carries a discriminator (its label, else its
month-day), the read dedups on the full UID, and the query is ordered by
Data._ID so a genuine duplicate resolves the same way every time.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 23:04:06 +02:00
8881559e67 fix(contacts): scope managed-event queries + fix reminder fire hour
Two provider-level bugs in the managed-event data path:

- queryManagedEvents and applyManagedCalendarReminders matched every event in
  the calendar (UID_2445 IS NOT NULL / no filter). A stray user event there
  (e.g. an .ics import) was treated as 'existing but not desired' and deleted,
  or had its own reminders wiped and all-day-re-encoded. Both now match only
  our own mirror events (the 'contact-' UID prefix).

- All-day reminder offsets were sampled at the event's DTSTART, which for a
  year-less birthday is the 1972 leap anchor — a year whose timezone offset
  (pre-DST) differs from today's, skewing every modern occurrence by up to an
  hour. The offset is now sampled at the upcoming occurrence (nextYearlyOccurrence),
  leaving only the inherent ±1h DST drift.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 23:03:51 +02:00
9f7e93d5f8 fix(contacts): seal managed calendars off from user edits
Managed special-dates calendars were only recognised by an id cached in
preferences, and were offered as ordinary write targets — so a user could
create an event in 'Birthdays' (which the next sync then deleted), and after
a backup restore wiped the prefs the editor lock silently disappeared.

CalendarSource now carries isManaged, read from the durable CAL_SYNC2 marker
the data layer already stamps, so identity survives a restore. With it:
- the new-event calendar picker excludes managed calendars (no user events
  land in a calendar the sync owns);
- the editor lock keys off the marker, not a stored id;
- a managed event's save is forced to the whole series instead of parking in
  the scope dialog (a 'this/following' split creates rows the sync reverts);
- the detail sheet hides Delete for managed events (the sync would just
  resurrect them — a contact date is removed at its source).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 23:03:33 +02:00
3863e74857 fix(settings): quick-switch write races, override parsing, stale custom fonts
Four fixes across the settings/prefs layer:

- Quick-switch toggles and reorders were read-modify-write against the
  async-echoed UI snapshot, so two rapid interactions reverted each
  other. Writes now go through SettingsPrefs.updateQuickSwitch, an
  atomic transform over the currently-stored value, via intent-level
  ViewModel ops; the MIN_ENABLED floor is re-checked inside the
  transform since the screen's guard reads the stale snapshot.

- parseReminderOverrides treated any unparseable stored value as an
  explicit empty override (no reminder). Only the deliberate 'none'
  sentinel means that now; garbage drops the entry so the calendar
  inherits the global default. Partially-valid values salvage their
  valid minutes.

- Replacing an already-active custom font never refreshed typography:
  the unchanged 'custom' token made AppFontSettings value-equal, so the
  StateFlow never re-emitted. A per-role import stamp now breaks
  equality on re-import (missing key = 0, backward compatible).

- The FontPicker custom preview resolved the font file unmemoized on
  every recomposition (disk stat + fresh FontFamily defeating the
  typeface cache); it's now remembered, keyed on the import stamp.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 23:01:58 +02:00
44d056f31f fix(settings): commit drag reorders unconditionally on release
The order mutation and onReorder call sat after the 160 ms settle
animation inside the same cancellable coroutine; grabbing another row
within that window cancelled the job between animation and commit,
silently reverting a finished reorder. The commit now runs
synchronously in onDragEnd — the settle is purely visual (the live
offset is re-based onto the new slot in the same frame, so nothing
jumps) and safe to interrupt.

Also derive the hovered target slot via derivedStateOf so a drag only
recomposes the column when a slot boundary is crossed, not on every
pixel (the held row's own translation is already draw-phase).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-02 23:01:58 +02:00
5c513a1b19 fix(settings): make the reminder picker's empty state safe and explicit
Three defects in the v2.13.0 multi-select reminder picker:

- Unchecking the last time on a per-calendar picker silently persisted
  an explicit no-reminder override; an accidental toggle-undo wiped the
  calendar's default. Empty-by-unchecking now reverts to 'Use default'
  (Inherit); deliberate no-reminder is its own exclusive 'None' row
  (reusing reminder_none) on both pickers, so the empty state is
  visible and reachable instead of implicit.

- A custom (non-preset) lead time's row vanished the moment it was
  unchecked, stranding the hand-entered value. Custom values seen this
  session keep their row (unchecked) until the picker closes.

- The optimistic selection seeded once from a possibly-not-yet-loaded
  settings state (initialValue emptyList behind a CalendarProvider-
  gated combine), so a quick first toggle after process-death restore
  overwrote the stored default. The local state now re-syncs from the
  incoming selection until the user first interacts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 23:01:41 +02:00
037d05b171 fix(reminders): close both gaps in disabled-calendar suppression (#17)
Two holes in the v2.13.0 'disabled calendars no longer notify' fix:

- A snoozed reminder bypassed the filter: ReminderActionReceiver's
  ACTION_SHOW posted unconditionally. The gate now lives once in
  ReminderNotifier.post — the single choke point both receivers use —
  built on a shared ReminderAlert.isForDisabledCalendar predicate that
  never treats calendarId 0L (pre-upgrade snooze intents without
  EXTRA_CALENDAR_ID) as disabled.

- A reminder firing while its calendar was disabled was lost forever:
  the receiver marks the full due set STATE_FIRED (deliberately, to
  stop provider re-broadcasts) and nothing re-scans. Suppressed alerts
  are now stashed in a DataStore-backed SuppressedReminderStore and
  re-posted when the calendar is re-enabled, while the event hasn't
  ended yet; expired entries are purged opportunistically.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 23:01:41 +02:00
86d4e11584 refactor(contacts): simplify the sync engine's gates and context
- The worker no longer pre-checks specialDatesEnabled: the engine already
  gates on it, and the worker now consumes the previously-unreachable
  Disabled result instead (without recording a run).
- ReminderContext drops the timed defaults it could never use — managed
  events are always all-day — saving two DataStore reads per sync.
- createCalendar no longer persists the calendar id itself;
  reconcileCalendars is the single place ids are recorded (it always
  followed up with the same write).
- The title-template fallback uses the shared resolveTitleTemplate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 22:40:44 +02:00
67a07a9346 refactor(settings): dedupe override decoding + on-convention template input
- choiceFor moves next to CalendarReminderOverride in SettingsPrefs and
  replaces the hand-rolled null/empty/else decoder in SettingsViewModel,
  so the picker and the special-dates rows can't drift apart.
- The special-dates template dialog used the codebase's only
  OutlinedTextField; it now follows the documented dialog convention
  (InlineTextField over a tonal surface, see DialogControls).
- reminderChoiceLabel drops the manual loop (and its wrong comment —
  map is inline, so composable calls are fine in its lambda).
- The blank-template fallback is shared with the sync engine via
  resolveTitleTemplate instead of being copy-pasted.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 22:40:44 +02:00
b8a420bdab refactor(contacts): one Context.hasContactsPermission() helper
The READ_CONTACTS check was written out three times (data source, RootScreen
resume trigger, settings screen). A single extension in the contacts package
mirrors the existing Context.hasCalendarPermission() precedent; the settings
call site follows in the settings cleanup commit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 22:40:44 +02:00
442697d57c refactor(calendar): move the colour palette to data with named entries
AndroidSpecialDatesCalendarSpec hardcoded three ARGB literals that its KDoc
claimed were 'picked from the shared palette' — a silent-drift hazard, and
referencing ui.common from data would invert layering. The palette now lives
in data/calendar as CalendarColorPalette with named hues; the swatch pickers
use CalendarColorPalette.all and the spec references Purple/Red/Blue.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 22:40:26 +02:00
8e3a17fdc3 refactor(calendar): share the insert values builder + reminder seeding
insertManagedEvent duplicated ~45 lines of insertEvent (ContentValues build
and the reminder-seeding loop). Both now go through buildEventInsertValues
in EventWriteMapper and a shared seedReminders, differing only in the UID
they stamp and the colour/attendee columns insertEvent layers on top.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 22:40:26 +02:00
e6dd627c85 fix(i18n): drop code-pushed German font strings
German translations must flow through Weblate; strings committed from
code conflict with Weblate's repo state on its next merge. The nine
font-picker strings from the custom-fonts feature fall back to English
until translated in Weblate (MissingTranslation is informational).

The stale-key removal (reminder_custom_with_value) stays: ExtraTranslation
is a fatal lint error, so deletions of retired source strings must
accompany the code change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 22:36:07 +02:00
Weblate
5844539db2 Translated using Weblate (Italian)
All checks were successful
Translations / check (pull_request) Successful in 4s
CI / ci (pull_request) Successful in 3m58s
Currently translated at 100.0% (363 of 363 strings)

Translation: Calendula/Strings
Translate-URL: https://weblate.dev.jeanlucmakiola.de/projects/calendula/strings/it/
2026-07-02 13:20:25 +00:00
Sam Gamgee
2cc83d7714 Translated using Weblate (Italian)
Currently translated at 100.0% (363 of 363 strings)

Translation: Calendula/Strings
Translate-URL: https://weblate.dev.jeanlucmakiola.de/projects/calendula/strings/it/
2026-07-02 13:20:25 +00:00
iker Contreras
dada1bbe72 Translated using Weblate (Spanish)
Currently translated at 100.0% (363 of 363 strings)

Translation: Calendula/Strings
Translate-URL: https://weblate.dev.jeanlucmakiola.de/projects/calendula/strings/es/
2026-07-02 13:20:22 +00:00
Weblate
e061cbe127 Translated using Weblate (Spanish)
Currently translated at 100.0% (363 of 363 strings)

Translation: Calendula/Strings
Translate-URL: https://weblate.dev.jeanlucmakiola.de/projects/calendula/strings/es/
2026-07-02 13:20:19 +00:00
262b2b8273 Merge pull request 'feat(theme): user-selectable custom fonts (#19)' (!55) from feat/custom-fonts into release/v2.13.0
Reviewed-on: #55
2026-07-02 08:40:49 +00:00
efca0c86c4 feat(theme): filter custom-font picker to font MIME types
All checks were successful
Translations / check (pull_request) Successful in 29s
CI / ci (pull_request) Successful in 5m5s
Restrict the "Choose file…" document picker to font MIME types
(font/ttf, font/otf, font/sfnt, font/collection plus legacy
application/* font aliases) instead of */*, so only fonts are
selectable. CustomFontStore's Font.Builder validation still guards
anything a provider mislabels.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 10:27:03 +02:00
08cd95cfba Merge remote-tracking branch 'origin/release/v2.13.0' into feat/custom-fonts
# Conflicts:
#	app/src/main/java/de/jeanlucmakiola/calendula/data/prefs/SettingsPrefs.kt
#	app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsViewModel.kt
2026-07-01 15:58:12 +02:00
bfc9a0db39 feat(theme): user-selectable custom fonts (#19)
Add a per-role font picker in Appearance settings. The two Material
typeface roles can be set independently: brand (display/headline) and
plain (title/body/label). Each can be the system default, a bundled
font (Atkinson Hyperlegible, Lora, JetBrains Mono), or a .ttf/.otf the
user loads from the device. The choice rebuilds the app-wide typography
in MainActivity; a null/"system" choice leaves the Material default.

Per the issue discussion the font-size and font-colour parts were
dropped — Android's accessibility font scaling and the dynamic-colour
theme already cover them.

Custom files are copied into app-private storage and validated with
android.graphics.fonts.Font.Builder before replacing the previous one,
so a bad pick can't wedge global text rendering; a missing/unreadable
file degrades to the system font. Each bundled font is previewed in its
own face in the picker. OFL licences + attribution vendored under
licenses/fonts/.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 15:12:47 +02:00
08905990cc Merge pull request 'feat(views): customizable quick-switch cycle + drawer order (#24)' (!54) from feat/quick-switch-view-config into release/v2.13.0
Reviewed-on: #54
2026-07-01 12:09:53 +00:00
b5895b190e Merge remote-tracking branch 'origin/release/v2.13.0' into feat/quick-switch-view-config
All checks were successful
Translations / check (pull_request) Successful in 5s
CI / ci (pull_request) Successful in 8m18s
2026-07-01 13:27:13 +02:00
37f0e22911 fix(views): correct snapping + animation for reorderable view lists
Rework ReorderableColumn: the first cut measured each row, but grouped
cards have position-dependent padding, so slot heights varied and rows
snapped to the wrong places with no animation. Now rows are a fixed
uniform pitch, the target slot is the whole-pitches dragged, neighbours
slide aside with a spring, and the held row settles onto its slot before
the order commits. Add a lift (scale + shadow) on the held row and
cancel an in-flight settle if a new drag pre-empts it.

GroupedRow gains gapBelow so the reorderable list can own uniform
inter-row spacing.

On-device verified on Pixel 10.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 13:25:06 +02:00
4e125e58d5 feat(contacts): show source year instead of age in titles
A FREQ=YEARLY event has one static title, so a per-occurrence age is impossible
without heavy per-year exception rows. Replace {age} with {year} — the birth
year (or an anniversary's start year) — which is static and correct on every
occurrence, and shows everywhere (widgets, other apps, exports).

- renderSpecialDateTitle substitutes {year}; drop the age snapshot computation
  and the sync `today` parameter.
- Default templates become "{name}'s birthday ({year})" / "…anniversary ({year})".
- Rename the setting to "Show year" (prefs specialDatesShowYear) and remove the
  now-unneeded {age} snapshot disclaimer in the template editor.
- Update tests, CHANGELOG and the design-doc note.

lint + unit tests + assembleDebug green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 13:19:04 +02:00
4c26450ad4 Merge pull request 'feat(widget): header navigation + fix R8-broken month arrows (#18, #20)' (!53) from feat/widget-header-navigation into release/v2.13.0
Reviewed-on: #53
2026-07-01 10:58:07 +00:00
52816b327a feat(contacts): per-type title format in the special-dates section
The "Title format" row was only editable for Birthdays; move it into each
per-type card so Anniversaries and Other dates get their own editable template
too (the dialog was already keyed by type). Each enabled type card now reads
toggle → title format → reminders.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 12:54:11 +02:00
cbb241bc4d feat(contacts): calendar-wide reminders for special-dates calendars
Managed calendars now treat reminders as a calendar-level setting instead of the
seed-once-per-event model, matching how these homogeneous birthday/anniversary
calendars are actually used.

- New per-type "Reminders" control in the Contact special dates section. Changing
  it persists the per-calendar all-day override (so new events match) AND
  re-applies the set to *all existing events* in that calendar
  (CalendarDataSource.applyManagedCalendarReminders → SpecialDatesSyncEngine
  .applyReminders), encoding each event's all-day offset from its own date.
- Settings → Notifications: the contact-date calendars no longer offer a
  per-calendar override row; they show a link that jumps to the Contact special
  dates section (managedCalendarIds now in SettingsUiState).

Tests cover applyReminders (override persisted + bulk-apply invoked, and None
clears). lint + unit tests + assembleDebug green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 12:41:52 +02:00
e5aa90bdd6 docs(changelog): note month-widget day tap opens that day (#18)
All checks were successful
CI / ci (pull_request) Successful in 9m29s
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 12:37:42 +02:00
580dc5a669 feat(widget): tap anywhere on a month-widget day to open it (#18)
Previously only the small day-number was a tap target, so tapping the
blank area of a day cell (or its "+N" overflow) did nothing. Make every
non-event part of a day column open that day — the day number, the empty
lane cells, and the overflow row — via a shared openDayAction, so a tap
anywhere on a day opens it, matching the in-app month grid. Event bars
keep opting out to open their own detail.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 12:23:33 +02:00
b9e800e9fc feat(views): customizable quick-switch cycle + drawer order (#24)
Add a "Views" settings section that lets users pick which views the
top-bar quick-switch button cycles through and drag to reorder them,
plus an independent drag-to-reorder for the navigation drawer's view
list. Two separate configs: a view turned off in the quick-switch
cycle is still reachable from the drawer, which always lists every view.

- QuickSwitchConfig (order + enabled set) and a drawer order persisted
  in SettingsPrefs (comma-joined enum names; "!" marks a disabled view).
  Missing views append enabled and unknown names drop, so a future view
  defaults into both lists.
- The pill cycles through the configured enabled views in order; the
  drawer renders CalendarDrawer from the drawer order. Both threaded
  from CalendarHost via CalendarHostViewModel.
- New ReorderableColumn: dependency-free, measurement-driven drag
  reordering for the short grouped-card settings lists (no LazyColumn,
  since settings are a single verticalScroll column). Commits one write
  per gesture.
- The switch needs two targets, so the last two enabled views can't be
  turned off.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 12:19:50 +02:00
40d70d9f2b feat(widget): open the app from the widget headers (#18, #20)
Tapping a widget header now opens the app on a sensible top-level view,
via a shared WidgetNavRequest.OpenView(view?) — a concrete view roots
there over the default home, a null view resolves to the default home:

- Month widget: the month/year title opens the month view (#18). Its
  "today" button now snaps the grid back to the current month in place
  (the reset that used to sit on the title), so paging + jump-to-today
  are both reachable and the title is free to open the app.
- Agenda widget: the "Upcoming" title opens the default view (#20), so
  users whose home view is Week/Month can reach it in one tap instead of
  drilling through a day or event.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 12:02:14 +02:00
14b19cd902 fix(widget): keep Glance ActionCallback classes under R8 (#18)
The month widget's prev/next/today controls and the agenda widget's
refresh run through Glance `actionRunCallback<T>()`, which persists the
callback's class name into the click PendingIntent and instantiates it
reflectively when tapped. Under R8 full mode (AGP 9 default) those
callback classes — only ever referenced reflectively — were renamed and
lost their no-arg constructor, so the lookup failed silently and the
arrows did nothing on release builds (while actionStartActivity taps,
which need no reflection, kept working).

Keep every ActionCallback's name and constructor. Verified against the
release mapping: ShiftMonthAction/ResetMonthAction/RefreshAgendaAction
now retain their original fully-qualified names.

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