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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
Extract the disabled-calendar filtering from EventReminderReceiver into a
pure postableAlerts() function and unit-test it, since the receiver flow
itself is not reachable from a plain JUnit test.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reminders fired for every due CalendarAlerts row regardless of whether
the event's calendar had been disabled in-app, even though disabled
calendars are hidden from every other surface. Carry the calendar id
through the alert and filter disabled calendars before posting, while
still marking all due alerts fired so the provider stops re-broadcasting
the suppressed ones.
Closes #17
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Make a debug install unmistakable next to the production app, Flutter-style:
- src/debug resource overrides: "Calendula Debug" label and a burnt-orange
launcher-icon background (production stays slate). The .debug applicationId
suffix already lets both install side by side.
- DebugRibbon: a "DEBUG" corner ribbon overlaid in MainActivity, gated on
BuildConfig.DEBUG so release/releaseTest never show it. Enables the
buildConfig feature for the flag.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Saving "only this event" on a recurring event inserts a modified-occurrence
exception at Events.CONTENT_EXCEPTION_URI. buildOccurrenceExceptionValues set
the occurrence's end as DTEND, but the provider rejects that outright —
CalendarProvider2.checkAllowedInException throws
IllegalArgumentException: Exceptions can't overwrite dtend
so the insert failed, the save surfaced as SaveUiState.Failed, and the edit
screen reappeared with no change applied. "This and following" and "all events"
never go through the exception insert, which is why only "only this event" broke
(Codeberg #16, verified on a Pixel 10 / Android 16).
An exception is a single instance whose end the provider derives from
DTSTART + DURATION (clearing the inherited RRULE itself), so carry the length as
DURATION and drop DTEND — the same shape AOSP Calendar/Etar use. Verified
on-device: editing one occurrence now applies to just that occurrence and leaves
the rest of the series untouched.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Codeberg #14. Event-level multi-reminders already worked; this widens the
*defaults* layer from a single Int to a List<Int> so a calendar's default can
carry several lead times (e.g. a birthday calendar: one week before *and* on
the day).
- SettingsPrefs: global + per-calendar timed/all-day defaults become List<Int>;
CalendarReminderOverride.Minutes(List<Int>); resolveDefaultReminder returns a
list. Storage stays backward-compatible — a legacy single value ("30") parses
to [30], comma-joined for multiples, "none"/empty for no reminder.
- ReminderDefaultPicker is now multi-select: M3 Checkbox rows over the existing
grouped-tonal idiom, an exclusive "use default" group for per-calendar
overrides, and a Custom row that adds an arbitrary lead time to the set.
Optimistic local state so quick successive toggles don't race the settings
flow round-trip.
- New events seed their reminder list from the resolved default.
Tests cover none/single/multiple round-trips, legacy single-value parsing, and
per-calendar list overrides. lint + test + assembleDebug green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Issue #12 asks for hiding finished events in the agenda view *and* its
home-screen widget. The in-app screen already honours the Past events
setting (Show / Dim / Hide); extend the same setting to the "Upcoming"
agenda widget.
The widget loads its events unfiltered and applies the mode reactively in
the composition against a captured "now" — mirroring how the range is read
from per-instance Glance state — so toggling the setting reflects on a live
widget without relying on provideGlance's data preamble re-running. Hiding
drops finished events and any day they empty; dimming fades the colour
stripe and lowers the title's emphasis (Glance has no generic alpha
modifier). setPastEventDisplay now pushes the mode into each widget's Glance
state and recomposes, like setAgendaWidgetRange.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bump versionName to 2.12.0 (versionCode 21200) and add the changelog.
Merging this into main triggers release.yaml, which builds, signs,
publishes and mints the v2.12.0 tag + Gitea release.
Closes #13
Overlapping events split a day column into narrow lanes where the title
was clipped to one or two characters. Mirror Google Calendar: drop the
time label on overlapping (multi-lane) blocks and wrap the title across
as many lines as the block height allows, so the full title stays
readable without opening the event.
Closes #13
Adds two independent display settings under Settings › Appearance, both
defaulting to the current behaviour (off) so nothing changes until a user
opts in:
• Agenda › "Past events" (Show / Dim / Hide) — events that already ended
today can be left as-is, faded, or dropped from the list. Hiding also
removes any day left empty, falling back to the empty state. Re-evaluated
each minute so rows fade/fall away as they end while the screen is open.
• Calendar › "Dim completed events" — a separate toggle that fades finished
events in the month and week grids, kept independent of the agenda setting.
An event counts as completed once its end is at or before now (in-progress
events are never dimmed; all-day events only after their day is fully over),
via a shared EventInstance.hasEnded(now). The grids read the cut-off through
a new LocalDimCutoff CompositionLocal (mirroring LocalShowHourLines) so only
the event chips recompose on the per-minute tick, and only while dimming is on.
Also adds an "Agenda" section header so the agenda rows stand apart in the
now-busier Appearance screen, and documents the feature in the changelog.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Opening the new-event form now puts the cursor in the title field and raises the
keyboard, so the user can type the name straight away instead of tapping the
field first (issue #10). On by default — most events get a title — with a new
"Focus title on new event" switch in Settings → New event form to turn it off.
Only the create form auto-focuses: editing an existing event and opening a
prefilled/imported form never grab focus (guarded by !isEditing && title blank).
- SettingsPrefs: autofocusEventTitle (booleanPreferencesKey), default true.
- Plumbed through SettingsViewModel/UiState (settings switch) and
EventEditViewModel/UiState (read by the form).
- EventEditScreen: a FocusRequester on the title InlineField, requested once per
open from a LaunchedEffect when the guard holds.
- Strings (en + de), unit test for the new pref default/round-trip.
Bumps to 2.11.2.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Calendula didn't declare the intent filters launchers and the system use for
calendar actions, so it never appeared in the "default calendar app" chooser —
on every platform, not just GrapheneOS (issue #9). Android exposes no API for an
app to set itself default, so registering these filters is the only way users can
pick it from the system picker.
Adds to MainActivity:
- MAIN + APP_CALENDAR — the "open the calendar app" action the OS/launchers use.
- VIEW on content://com.android.calendar/time/<epochMillis> and the time/epoch
mime type — a launcher/clock date tap. The provider's time Uri is parsed into a
LocalDate and opened on the day view, rooted over the default home view (a new
sourceless WidgetNavRequest.OpenDate). The .ics import path now ignores the
calendar provider host so a date tap isn't mistaken for a file to import.
Bumps to 2.11.1.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Per review, it shouldn't be a top-level Settings entry: the "Help translate"
link now sits at the top of the full-screen App language picker (OptionPicker
gains an optional header slot). Updated README + changelog wording to match.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Invite community translations: a new Settings > Help translate row (next to
App language) opens the project's Weblate engage page, plus a Translations
section in the README. Documented in the 2.11.0 changelog.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Turning automatic backup off only cancelled the periodic work, so a
run-now that kept failing (e.g. its folder was deleted) retried forever
and spammed failure notifications.
- Cancel the immediate "run now" work too when backup is disabled.
- Worker no-ops (no retry) when the toggle is off, so already-queued work
can't revive itself.
- Reconcile scheduled work against saved settings on every app launch —
re-arms after a reinstall and clears orphaned work once backup is off.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Scheduled one-way export of local calendars to a user-chosen folder,
overwriting calendula-backup.ics each run — the manual backup, automated.
Not a sync; stays INTERNET-free (provider reads + local file write).
- WorkManager periodic job (deps: work-runtime-ktx, documentfile); worker
pulls collaborators via a Hilt @EntryPoint, so no custom WorkerFactory
wiring. First periodic run is delayed one interval so it can't race the
immediate "run now" feedback run; the writer also overwrites the canonical
file and cleans up any "(1)" duplicates from earlier races.
- SettingsPrefs: enabled, interval (minutes, floored at 30), folder Uri,
last-run status; persisted SAF write grant.
- UI in Calendars > Backup: toggle, folder picker, amount+unit interval
dialog, last-run status line. Notifies after repeated failures.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the per-account toggle switch and nested manage row with a single
trailing overflow (⋮) menu holding both account-level actions:
- "Enable all" / "Disable all" — toggle every calendar in the group
- "Manage in app" (synced) / "Add calendar" (local)
The dropdown is styled to fit: rounded corners, a distinct floating
surface (surfaceContainerLowest + lifted shadow) so it stands clear of
the cards, and a divider separating the two actions. Shortened the manage
label to "Manage in app" and dropped the now-unused account a11y strings.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Settings:
- Event-form field rows show each field's own icon (shared
EventFormFieldVisuals, reused by the editor and settings)
- Move "Add Quick Settings tile" to a top-level Settings hub row
- Notifications: reliable-delivery + snooze moved above the
per-calendar block, which now folds behind one expandable section
Calendars manager:
- Local and synced calendars now use one collapsible group card
- Source-branded headers: each account shows its app's launcher icon
(Google Calendar, DAVx5, …) loaded from PackageManager; local shows
a device chip
- Per-account toggle-all switch (CalendarsViewModel.setAccountDisabled)
- Management action ("Add calendar" / "Manage in app") is a nested row
inside the expanded group; removed the dead "Add account" row
- A fully deactivated account dims its header, not just the switch
- Group headers use the normal row colour; their options sit one tone
darker (GroupedRow gains an optional container colour)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>