- Restore is import, not export: offer it whenever any writable, non-
managed calendar exists (local or synced), not only when there is a
local calendar to back up. Previously the row lived inside the
export-gated block and vanished for users with only a writable synced
calendar, despite import supporting that target.
- Export-picker selection now uses rememberSaveable and is no longer
keyed on the observer-driven calendars list, so a background provider
re-emit (sync/recolor) can't silently reset the user's de-selections,
and the choice survives rotation.
- Shared calendar picker: restore the displayName fallback for a synced
calendar whose account name and type are both blank (was grouping them
under an empty header).
- Drop imports left dead by the CalendarPickerGroups extraction.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Move the multi-event import's confirm button into the app-bar actions so
it's reachable without scrolling past a long calendar list, and put the
count in the title ('Importing 5 events') instead of a separate 'N events
in this file' line. Hoists the selected target calendar to the screen so
the top-bar action can read it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The import de-dups by UID against the target calendar (idempotent restore),
so re-importing events already present shows a low 'Added' count. Add a note
under the title when any were skipped so the outcome doesn't read as broken.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Tapping Export with more than one exportable calendar now opens a picker
to choose which local calendars to include (all selected by default); a
single calendar exports straight through as before. Threads an optional
calendarIds filter through exportEvents/exportableEvents (null = all
eligible), so the auto-backup path is unaffected. The backup section is
now gated on there being at least one exportable (non-managed) calendar.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the plain centered text list with an M3 Expressive success state:
a tonal check badge that springs in, the headline, and big-number tonal
stat tiles for added / duplicate-skipped counts, with a full-width Done
button. Stat tiles carry the full-sentence plurals as accessibility
labels so TalkBack still reads 'Imported N events'.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a 'Restore from .ics file' row to the Calendars backup section, next
to Export. It opens a SAF document picker and routes the picked Uri into
the existing import flow (parse, dedup by UID, target-calendar picker,
summary) via CalendarHost's importUri — the same path an externally
opened .ics already takes, so no new import machinery is needed.
Closes #32.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>
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>
A FREQ=YEARLY event has one static title, so {age} is only a sync-time snapshot
and can look wrong on far-future occurrences. Keep it out of the default
templates ({name}'s birthday / anniversary), and when a user does add {age} in
the title-format editor, show a short disclaimer about the snapshot behaviour.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- German translations for the special-dates sub-page, the managed-field editor
hint, and the calendar/title strings.
- Store listing (en/de): a paragraph on the optional, on-device, one-way contact
special-dates feature.
- Soften the calendar-permission rationale ("all it asks for up front") now that
an optional contacts permission exists, without weakening the privacy promise.
- CHANGELOG entry for #15.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A dedicated sub-page under Settings drives the mirror:
- master enable toggle — requests READ_CONTACTS contextually (only here, never at
startup), runs an immediate sync on enable, and on disable confirms then tears
the managed calendars down;
- per-type toggles (Birthdays / Anniversaries / Other), each confirm-guarded
since turning one off deletes its calendar;
- an editable title template ({name}/{age}) and a Show-age toggle;
- a "Sync now" row with the last-run time, a paused/permission banner with a
re-grant button, and a pointer to per-calendar colour/visibility/reminders.
Exposed via a separate SpecialDatesUiState flow (the main settings combine is
already at capacity); the ViewModel gained the engine/scheduler wiring.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When editing an event in a special-dates calendar, the title, all-day/date and
recurrence are locked (the sync overwrites them) while reminders, location, notes
and colour stay editable — the field-level "managed" contract from the design.
- EventEditUiState.isManaged (calendar id ∈ managedCalendarIds while editing),
wired through the ViewModel's state combine.
- InlineTextField/InlineField gain an `enabled` flag (dims + goes read-only);
ScheduleRow dims and disables its date/time taps; the all-day switch and the
recurrence card are disabled.
- A one-line "Managed by …" hint under the title explains what's editable.
The calendar picker needs no change — it's already create-only, and managed
events are always edits.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The engine reconciles device contact dates into the per-type local calendars.
Each run is an idempotent diff keyed on the deterministic UID_2445:
- new contacts inserted (seeding reminders via resolveDefaultReminder, plus a
useful per-calendar all-day default of on-the-day + a week before so birthdays
get lead time out of the box);
- changed contacts get a targeted managed-column update (title/dtstart/rrule
only) — reminders/location/notes are never re-touched, so user edits survive;
- removed contacts deleted.
Managed calendars are created/adopted/removed per enabled type (reconcileCalendars,
self-healing against a stored-id that no longer exists), all-day FREQ=YEARLY
events anchored at the birth year (or a leap anchor when year-less). Pure helpers
(uid, anchor, age snapshot, title templating) and the diff are extracted for
unit testing; a stateful fake exercises full-run idempotency and user-data
preservation.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>