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>
The agenda widget loaded and sliced its data in the provideGlance preamble
and captured it as a non-reactive local. updateAll() reliably recomposes a
live Glance session but does not reliably re-run that preamble, so a range
change was redrawn against the stale slice (intermittently — only when no
session was alive did it pick up the new range). This is the same platform
limitation the month widget already works around.
Mirror that pattern: load the widest selectable window once, store the range
in per-instance Glance state (AGENDA_RANGE_KEY), read it reactively via
currentState, and slice in the composition. The settings setter writes the
state into each instance and recomposes, so a range change now reflects via
plain recomposition regardless of session lifecycle.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The debounced observer was unreliable: drop(1) plus the ViewModel lifecycle
could swallow a change after re-entering the app, so a single range change
sometimes didn't refresh the widget. Refresh directly from the settings
setters instead (always fires on a real change), serialized through a mutex
so a rapid flip-and-flip-back can't run two updateAll calls at once and
strand the widget on the intermediate value.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Refreshing the widget directly from the setter raced when a setting was
flipped and flipped back quickly: two concurrent updateAll calls could
coalesce around a stale read and leave the widget on the intermediate value.
Observe the agenda-widget-range and week-start prefs instead and push a
single debounced updateAll once changes settle, so the widget always
converges to the final value.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The agenda widget only re-read agendaWidgetRange on the next data-change /
midnight / periodic refresh (or re-placement), so a settings change appeared
to do nothing until then. Push an updateAll from the settings setters:
the agenda widget on an agenda-widget-range change, and both widgets on a
week-start change (month weekday header + the agenda widget's "this week").
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Consolidate the separate "range title" and "range button" settings into a
single "Range bar" toggle, now that the header and switcher live on one bar.
On by default.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Move the session range pill from the bottom-left corner onto the same top
bar as the "Showing all upcoming events for …" header — header on the left,
pill on the right, each still independently toggleable.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reword the range header to "Showing all upcoming events for". Replace the
dry empty state ("Nothing scheduled" + subtitle) with a single warmer line
("You're all caught up"), drop the subtitle, and switch the icon to a
coffee cup. The empty title is shared with the agenda widget.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Move the "Showing all events for <window>" line out of the picker and into
the agenda view as an always-visible header above the list. Add two
settings (Agenda group, both ON by default) to toggle the range header
banner and the bottom-left range pill independently. The picker keeps its
two-list grouping but no longer carries the header.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Group the range options into calendar-aligned (today / this week / this
month) and rolling (next 7 / 30 days / custom) lists. Add a header showing
the concrete span currently in effect — a single date for Today, the month
and year for This month, otherwise a start–end span — so it's clear what
the agenda is showing. The header appears only where a window is supplied
(the agenda pill), not in Settings.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a tonal pill in the agenda's bottom-left corner showing the current
range; tapping it opens the existing AgendaRangePicker as a session-only
override. The override lives in AgendaViewModel (in-memory), so it survives
view switches and rotation but resets to the saved default when the app is
relaunched. The pill fills with the primary container while an override is
active, and a hint in the picker spells out the temporary nature.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drop the uniform two-per-card grouping for meaningful sections: theme &
colour (2), calendar — default view, week start, time format, hour lines
(4), and agenda (2).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Integrate #3/#5/#6 (week-start, time-format, hour-lines) into the agenda
range branch. SettingsViewModel folds all five view prefs into one combine;
WidgetData keeps the range-based window and adds the resolved is24Hour.
Also, in the same Appearance section:
- regroup the rows into four titled-by-spacing groups (theme & colour /
calendar layout / timeline display / agenda) instead of one long card.
- add an explanatory description line under the title of each agenda-range
picker (PickerDescription).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extend AgendaRange with calendar-aligned windows alongside the rolling
ones: ThisWeek runs through the end of the current week (respecting the
week-start preference — a Monday start means everything before next
Monday), ThisMonth through the last day of the current month. dayCount now
takes the anchor day and week-start; the agenda screen and widget resolve
the week-start preference and pass it through. Rolling options relabelled
("Today", "Next 7 days", "Next 30 days") to read distinctly from the new
calendar-aligned ones.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add independent agendaScreenRange and agendaWidgetRange preferences, each a
rolling window: 1 day / 1 week / 1 month / custom 1–365 days (default Month).
Rolling (not calendar-aligned) so the span never degenerates near a period
boundary. The in-app Agenda screen and the agenda widget each read their own
setting, configured via a new AgendaRangePicker with an inline custom-days
editor.
Closes #4 (Codeberg)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a showHourLines preference (default off — the existing clean look).
When on, the week and day timelines draw a faint outline-variant line at
each hour boundary, sitting over the column background but beneath event
blocks. The toggle is provided app-wide via LocalShowHourLines and applied
through a reusable hourSeparatorLines() modifier.
Closes #5 (Codeberg)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a TimeFormatPref (Auto/12h/24h) preference. Auto follows the device's
24-hour system setting; the others force a clock app-wide. The resolved
convention is provided once at the app root via LocalUse24HourFormat, so
every in-app time label reads it without per-screen plumbing.
A shared, pure TimeFormat helper (formatTimeOfDay / formatMinuteOfDay /
formatHourLabel / timeOfDayFormatter) is now the single source for all
time-of-day rendering. Routing every site through it also fixes a
pre-existing inconsistency: the week/day timeline gutters and the agenda
screen + widget hard-coded 24h, while event detail/edit/search/reminders
followed the locale — so a 12h-locale user previously saw mixed formats.
Covered: week & day timelines (gutter + event blocks), agenda screen and
widget, event detail/edit, search, and reminder notifications.
Closes #6 (Codeberg)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the AUTO/MONDAY/SUNDAY week-start enum with a sealed type
(Auto + Day(DayOfWeek)) so users can pick any of the seven days as the
first day of the week. The picker now lists "Automatic" plus all seven
localised weekday names; labels come from java.time display names rather
than per-day string resources.
Stored values round-trip by DayOfWeek.name, so the legacy MONDAY/SUNDAY
preferences migrate transparently. Garbage values fall back to Auto.
Closes #3 (Codeberg)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>