Lay out all-day events as bars across their start→end day columns, packed into
lanes, instead of listing a multi-day event separately in each day's column
(mirrors the month view and iOS).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- gear now opens a full-screen Menu screen (Profile / Appearance / Accounts /
Groups / Sync / Switch server / Logout), replacing the bottom-sheet menu that
sat partly under the system nav buttons. Each entry opens its own full-screen
page and returns to the menu on close.
- settings 'Darstellung' is the appearance table only again (no long scroll to
reach navigation)
- much more compact settings: dropdowns use a small outlined chip instead of a
full-height OutlinedTextField, smaller sync icons, bodyMedium row labels
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- move the top-bar hamburger to the LEFT (drawer opens from the left)
- much denser drawer: compact header, and calendar rows now use a small eye
toggle instead of the bulky Switch, with tighter padding/text
- tapping the gear now opens the full-screen Settings directly (no bottom-sheet
menu that sat under the system nav buttons, no redundant sync — that's in the
drawer header). Accounts / Groups / Profile / Switch server / Logout moved
into a 'More' section at the bottom of Settings.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Mirror the iOS Contacts birthday importer: read the address book's birthday
events and reconcile them into the user's birthday calendar, scoped to this
device via external_uid 'contact:<deviceId>:<contactId>' (add/update/delete),
then report the device.
- READ_CONTACTS permission + ContactsReader (Event TYPE_BIRTHDAY, handles
year/no-year date formats)
- API: GET /birthdays, POST /birthdays/sync-report; local-event create/update
now carry external_uid (+ update carries rrule/birth_year)
- CalendarViewModel.syncContactBirthdays reconciles like iOS; device-local
enable flag + stable device id in SettingsStore
- birthday dialog gains an 'Import from contacts' action (requests permission);
auto-syncs on launch when enabled
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Mirror the iOS side drawer on Android with a ModalNavigationDrawer opened by the
hamburger or an edge-swipe (replaces the calendar filter bottom sheet):
- header with avatar/name/server + manual sync, full menu (gear), and close
- view + group switcher chips
- flat calendar list: tap toggles visibility, long-press opens a context menu
(hide/banish + reminders), and a Sort toggle reveals up/down reordering.
Order is device-local (SettingsStore.calendarOrder, mirrors the web).
- month-switching is now a named dropdown (scroll vs pages) like web/iOS
- new device-local 'Hide menu button' setting (drawer opens via edge-swipe)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Redesign the settings screen into uniform rows (sync icon | name | value):
dropdowns replace the chip pickers, every colour gets a reset to a canonical
default, a per-row sync icon toggles that setting's cross-device sync, and a
global switch flips all. Enabling a flag adopts this device's current value.
- AppSettings/SettingsStore: add text/bg/line colours, cache_months,
month_view_paged, and the account-wide sync_flags map (+ DEFAULT_SYNC,
effective-merge on pull)
- CalendarRepository.updateSettings: partial PUT of only the synced values plus
the flag map (unsynced columns stay untouched server-side)
- Theme: fully dynamic — primary/accent tint, background/text/line drive
background/onBackground/outline (matching web); today/divider/label unchanged
- add hide-profile (directory_hidden) to the profile block (parity with web/iOS)
- language + text/line contrast stay device-local
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Play requires targeting a recent API level for new apps; bump compileSdk/
targetSdk to 35 and set the release version name to 1.0.0.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- one birthday calendar per user: BirthdayDialog targets the single calendar and
offers activation when none exists; VM birthdayCalendar()/ensureBirthdayCalendar()
- removed the birthday toggle from the generic new-calendar dialog
- exclude the birthday calendar from the group-visible ("shared calendar") picker
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- CalEvent parses is_birthday and exposes renderTitle (server age)
- EventLabel renders a cake icon + render title in month/week/day/agenda/preview
- LocalCalendar model + repository/api gain is_birthday, notify-days, rrule,
birth_year; AccountsScreen can create a birthday calendar with a notify picker
- FAB long-press menu -> "New birthday" opens a minimal name+date dialog
(year-unknown option) targeting a birthday calendar
- L10n strings
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New device-local setting "Monatsansicht seitenweise wischen" (SettingsStore,
no server sync). When off, the month view stays the continuous vertical scroll
feed. When on, it becomes a HorizontalPager — one month per screen, six
height-filling week rows, swipe left/right to change month. The prev/next/today
signals route to the pager (animateScrollToPage) instead of the list, and the
visible month is derived from pagerState.currentPage. WeekRow now takes a
rowModifier so the same row renders at a fixed height (scroll) or weight(1f)
(paged). Setting is re-read when the settings screen closes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1) Burger menu (and other bottom sheets) had rows hidden behind the system
navigation bar: MainActivity never called enableEdgeToEdge(), so the sheets'
navigationBarsPadding resolved to 0. Enable edge-to-edge and place the insets
ourselves — top bar statusBarsPadding, content/FAB navigationBarsPadding,
auth screens systemBarsPadding — and add navigationBarsPadding (+ imePadding
for the editor) to the Filter/Groups/Accounts/Editor sheets.
2) The calendar filter only listed calendars that had events in the loaded
range, so empty calendars couldn't be toggled. Load the full calendar list
from all sources (loadAllCalendars) and merge it with the event-derived
entries — matching iOS, which already shows the complete list.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The colour dot for a shared (non-owned) local calendar was disabled, and the
colour write hit the owner-only PUT /calendars/{id} (404 for recipients). Now
the dot is always editable and colour changes go through the dedicated
PUT /calendars/{id}/color endpoint, which stores a recipient's own per-user
colour server-side (synced across devices) without granting write or rename.
Renaming stays owner-only (no rename UI for shared calendars).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Three Android UX fixes reported from on-device testing:
1. A calendar shared with the user now shows read-only affordances: parse the
server's new read_only flag on events, show a lock icon in the filter sheet,
and exclude read-only shared calendars from the event editor's calendar
picker (getWritableCalendars) so saves no longer 403. The calendar's display
name (owner's name) comes from the server.
2. (server-side) group event name prefix removed — no client change needed.
3. Move the personal/group view switcher out of the crowded top bar into the
burger MenuSheet as a "group view" section (personal + each group with a
check mark), mirroring iOS. The GroupSwitcher pill and its top-bar wiring are
gone; the active-group banner stays.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The filter-sheet eye was the only hide control on Android and it hard-synced
to the server (sidebar_hidden/enabled) — so a quick, per-device hide leaked to
the web and other devices. Now Android matches iOS' two-tier model:
- Quick-hide (eye toggle) is device-local only (setCalendarHidden → hiddenKeys),
no server call.
- Banish ("permanently hide", new archive action per row) syncs sidebar_hidden
to the server and moves the calendar into Settings; a new "Hidden calendars"
section there re-enables it (server + auto-refetch).
- reconcileCalendarVisibility now drives banishedKeys (not hiddenKeys) from the
server's sidebar_hidden, so the reconcile never overwrites the local quick-hide.
- Filter sheet lists calendars from the unfiltered cache (minus banished), so a
locally hidden calendar still appears and can be toggled back on.
Colours and reminders stay server-synced as before. iOS already had this model
and is unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Hiding or showing a calendar on the web sets enabled=false + sidebar_hidden
server-side, so the server stops returning that calendar's events entirely.
The app synced the filter toggle TO the server (setCalendarHiddenSynced) but
never pulled the server's sidebar_hidden flags back, so a calendar hidden or
re-enabled on the web (or another device) was never reflected until a manual
sync or relaunch — the "hide it on the web and the app won't bring it back" bug.
Add reconcileCalendarVisibility(): pulls the account lists and updates the
local hidden set from the server's sidebar_hidden flags for external calendars
(local/ical keys untouched). It runs before the initial load, on manual sync,
and on a new ON_RESUME lifecycle hook — forcing a refetch when the set changed
so a re-enabled calendar's events reappear on their own. Mirrors the iOS fix.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
mergeIntoCache() unconditionally evicted all cached events in the fetched
range, so a transient sync failure (expired CalDAV credentials, an HA/Google
token-refresh failure) wiped the affected calendar until the next clean fetch.
Home Assistant was the most visible victim: a token failure aborts the whole
account fetch and the error arrives without a calendar_id.
Now sync errors are split into per-calendar keys (error carries a calendar_id)
and whole sources (account-level error without one). mergeIntoCache() keeps
matching cached events, and only extends the cached range on a fully clean
fetch so failed calendars are retried on the next load. Mirrors the iOS fix.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The server now includes calendar_id in per-calendar sync-error entries.
Match CalendarFilterSheet's warning icon on (source, calendarId) like
calendarKey() already does everywhere else, instead of a fragile
name-suffix comparison that could false-positive across accounts whose
calendar names happen to overlap.
loadVisible(force = true) (used by the error-banner retry button) was
still funneling into loadRange with an implicit force = false, so the
isCached short-circuit was never actually bypassed on retry.
- TimeGridView: remember Instant.now() (keyed on the day) and the day-header
DateTimeFormatter instead of rebuilding both on every recomposition/frame —
fixes flicker in the past-event dimming and avoids repeated allocation.
- CalendarViewModel.refreshFromCache: short-circuit the non-group filter pass
when both hiddenKeys and banishedKeys are empty, mirroring the existing
group-mode short-circuit.
- CalendarViewModel.setCalendarHiddenSynced: wrap the server PATCH in
runCatching and surface failures via state.error instead of silently
dropping them (the local toggle would otherwise diverge from the server
unnoticed).
- CalendarViewModel.afterMutation: refetch only the already-cached range
(force-bypassing loadRange's cache check) instead of invalidateCache() +
initialLoad(), which reloaded the entire ±cacheMonths window and froze the
UI on every single event save.
- CalendarrRoot: replace calendarVm!! with a null-safe ?.let, falling back to
the splash screen instead of crashing if the VM isn't ready yet.
- GroupsScreen: replace existing!!.icon!! / g.icon!! double force-unwraps
with safe-call chains (existing?.icon?.takeIf { GroupIcons.isKey(it) }).
Deliberately out of scope: R8/minify settings, MonthView's cache window
size (MONTHS_BACK/MONTHS_AHEAD), and mergeIntoCache's merge logic (already
reviewed as correct). No debounce added to setCalendarHiddenSynced — would
need new machinery beyond this fix's scope.
/api/caldav/events now returns an "errors" array alongside "events" for
calendars that failed to sync (e.g. expired credentials) while still
reporting success overall. Previously such failures were silently
swallowed, so a calendar could appear enabled/visible while showing zero
events with no explanation.
- CalendarRepository.fetchEvents now returns EventsResult(events, errors),
parsing the new "errors" array the same way importIcs's Triple return
handles multi-value results.
- CalendarUiState gains syncErrors, populated in loadRange's onSuccess.
- CalendarScreen shows a second error-style banner for sync errors,
additive to the existing network-failure banner.
- CalendarFilterSheet shows a warning icon next to calendar rows whose
source+name matches a sync error (best-effort suffix match, since the
server's error name is "<account> – <calendar name>").
The filter-sheet Switch now calls setCalendarHiddenSynced(), which
updates local hiddenKeys immediately AND fires a sidebar_hidden PATCH
to the server for caldav/google/homeassistant calendars. This makes
the quick-hide consistent with the web sidebar across all devices.
CalendarFilterEntry gains a source field so the sheet knows which
backend to target.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Notifications now show "in 30 Min. · Ort" instead of just the location,
giving users immediate context about how soon the event starts.
Default event duration picker moved from the View section to right below
the profile/privacy group, consistent with the iOS app arrangement.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add reminder editor (presets + custom number+unit) wired through save
- Per-calendar reminder mute: filter-sheet toggle + greyed-out editor hint;
reminders are kept, never deleted
- New AlarmManager-based NotificationScheduler + channel + POST_NOTIFICATIONS;
skips muted calendars
- New synced setting default_event_duration_minutes for new events
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Transparent background caused Pixel launcher to inject its own tinted
ring. Replace with #16703D (sampled from iOS icon background) so the
launcher shape-clips cleanly without any visible halo.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Without mipmap-anydpi-v26 XML, Pixel launcher wraps legacy PNGs in a
white ring template. Adding adaptive-icon XML with the iOS-matching
foreground PNG at correct per-density sizes (108dp canvas) eliminates
the nested-ring appearance.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Use Modifier.size(20.dp) instead of height(20.dp) so the spinner is
constrained on both axes and doesn't get cut off horizontally.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Material 3 menus use shapes.extraSmall (4dp default) which looked boxy;
bump to 14dp so all dropdown popups (view switcher, group switcher, ...)
get rounded corners matching the iOS look.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Long-press on a day now opens a preview popup (ported from the iOS
DayContextPreviewView): all-day events as colored bars with tapered
ends when they continue beyond the day, timed events as dot+time+title
rows, plus quick actions (new event, week view, day view)
- Month scroll performance: stable item keys + contentType in the
LazyColumn, remembered Instant.now()/theme colors, remembered callbacks
so visible week rows stay skippable during recomposition
- Fix "+N" and "KW" labels overlapping the lowest event lane (row bottom
padding 6dp -> 16dp, mirrors the iOS fix)
- Event detail: delete is now a quiet red text button instead of a
full-width filled container
- Top bar: hairline divider so the bar/calendar boundary is visible
- FAB: explicit circular shape
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CalEvent gains `reminders` (parsed from the server); AppSettings/SettingsStore
gain `defaultReminderMinutes` (null = off) and it's sent via updateSettings;
createLocalEvent/updateLocalEvent/eventBody and CalendarViewModel.saveEvent
thread `reminders` through. UI (editor + settings picker) and the local
notification scheduling follow next.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Group icons are semantic keys rendered as native Material icons (GroupIcons)
in the picker, group list, top-bar switcher and banner — mirroring iOS/web —
instead of OS emoji. Legacy emoji values render as a fallback. decorateGroup
fallback no longer prepends a glyph (server display_title is authoritative).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The calendar filter, in group mode, lists the group's members (+ the shared
group calendar) with toggles to hide each individually. Filtering is client-side
via CalendarViewModel.hiddenGroupKeys (gm:<id> / gc), reset on group switch;
members + colours loaded from the group detail.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CalEvent parses display_title; decorateGroup uses it (group icon + owner prefix
from the server) instead of the hardcoded glyph, with a fallback for older
servers. Raw title kept for editing.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Top-Bar-Gruppenumschalter ist jetzt eine tonale Pille (Akzentfarbe im
Gruppenmodus) statt eines flachen Icons – klar als Button erkennbar; das
Dropdown markiert die aktive Auswahl. Gruppen-Verwaltung: Tipp auf eine Gruppe
öffnet direkt deren Ansicht, Verwalten liegt aufs Zahnrad. Menü-Bottom-Sheet
respektiert jetzt das Navigationsleisten-Inset (+ Scroll-Fallback), sodass
Abmelden/Server wechseln nicht mehr hinter den Android-Buttons liegen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Neuer Gruppen-Verwaltungsscreen (Liste, Erstellen, Verwalten: Name, wählbares
Emoji-Icon, Mitglieder, server-definierte Mitgliederfarben, Löschen) über das
Menü. Top-Bar-Umschalter (Mein Kalender / Gruppen) + Akzent-Banner schalten den
Kalender in die kombinierte Gruppenansicht: Events laufen durch die normale
Cache-/Render-Pipeline (Monat/Woche/Tag/Termine), mit Namens-Präfix der
Besitzer und 👥 für Gruppentermine; im Gruppenmodus kein Sichtbarkeitsfilter.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
AccountsScreen überarbeitet: editierbare Farb-Punkte (lokal, iCal, sowie
Unterkalender von CalDAV/Google/HA via Farbwähler-Dialog); lokale Kalender
zeigen Gruppen-Marker + "geteilt von" und ein Aktionsmenü (Teilen/Importieren/
Exportieren/Löschen je nach Besitz & Berechtigung). Teilen-Sheet mit
Benutzersuche + Berechtigung; Import via OpenDocument, Export via CreateDocument.
Repository: importIcsFile(bytes) + Farb-Update-Methoden; ViewModel um Farb-,
Sharing- und Import/Export-Logik erweitert.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Einstellungen zeigen jetzt ein Profil-Kapitel: Anzeigename (editierbar),
Anmeldename (read-only), E-Mail + Speichern; Privatsphäre (private Termine
beschäftigt/verbergen) und geteilter Kalender (Dropdown der eigenen lokalen
Kalender) — serverseitig geladen und gezielt gespeichert.
Datenebene um Farb-Endpunkte ergänzt (lokal/iCal PUT + setCalendarColor für
caldav/google/ha) als Basis für die Farbbearbeitung.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Event-Detail zeigt "Erstellt von" (wenn != ich) + Privat-Hinweis; Editor hat
Privat-Toggle (nur lokale Kalender, durch saveEvent/Repo durchgereicht).
Login speichert userId + displayName (CredentialStore) für Vergleiche.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Past events (endDate before now) are drawn at reduced opacity across the month
bars, the week/day time grid (timed + all-day) and the agenda list when the
'dim past events' setting is on.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- System splash now uses a transparent icon, so there is no system icon that
shrinks into the in-app splash: just black, then the branded screen (icon +
Calendarr name + copyright), held until data is loaded
- Month/quarter title uses TextStyle.FULL (format style) with a hard fallback;
FULL_STANDALONE had the same desugar bug as the LLLL pattern (December showed
only the year)
- First open: load the full +/- cacheMonths window behind the splash and only
then reveal, so the app no longer enters mid-load and stutters
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Single system splash (core-splashscreen) kept on screen until the first
events load (StartupState), then reveals the ready app — removes the
two-stage 'icon then title' splash and the laggy half-loaded entry
- Month scrolling: dropped per-row BoxWithConstraints (measured grid width once
via onSizeChanged) and resolve bar colours once during packing instead of
every recomposition → much smoother scroll
- Serialize all event loads behind a Mutex and skip redundant state writes so
scroll-triggered month loads no longer pile up / thrash the UI
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Month view now draws multi-day events as continuous bars (lane packing,
per-week column span clipped at week boundaries, +N overflow) instead of a
chip on every day; events bucketed per week once + memoized packing for
smooth scrolling
- Startup: core-splashscreen covers the window from frame 0 (no warm-start
flash); the in-app splash stays until the first events finish loading (loading
starts behind the splash via an early-obtained CalendarViewModel + ready flow),
so you no longer enter a laggy app
- Background load shows a small spinner in the top bar (removed the linear bar)
- Week/Day titles wrap to two smaller lines (no more truncation)
- Event detail opens with a slide+fade animation
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- CalEvent parsing: handle Android's JSONObject.optString JSON-null quirk that
returned the string "null" — this made every event blue (color "null" →
fallback) and showed "null" for empty location/description. Now null-safe.
- Event detail: copy-to-calendar action; calendar row uses a calendar icon
(was identical to the notes icon); empty location/notes rows hidden
- Event editor: copy mode (prefill from an existing event, save as new)
- Settings: hour height, text/line contrast are now button chips (like the
cache range); color picker wraps, shows the current colour and a check mark
- Month scroll perf: precompute a day→events index once per change and only
reload when the visible month actually changes (was filtering every cell
every frame → laggy)
- Splash: dark window/splash background + inset launcher icon (was an ugly
white box on startup)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Month view rewritten as a continuous vertical scroll (iOS-style): no
prev/next buttons, compact fixed-height week rows, month label on the 1st,
top-bar title follows the visible month, Today scrolls back to today
- Long-press a day opens the event editor pre-filled with that date
- Theme: green container/tint colors so the FAB and accents are no longer the
Material default purple
- Event colors: parse #RGB shorthand, and fall back to a stable per-calendar
palette colour instead of a constant blue when the server omits a colour
- Top bar hides prev/next arrows in month view (scroll instead)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Theme primary now the iOS brand green (#20A050), fixed app-wide (was tied
to the server's blue primary_color)
- Launcher icon generated from the iOS AppIcon across all densities;
dropped the placeholder adaptive icon
- PasswordField: eye toggle + brief last-character reveal; used on login,
profile and CalDAV password inputs
- Login now surfaces the server's actual error detail (verified the JSON
contract against the live server; a 401 is a genuine credential mismatch)
- CLAUDE.md: correct prod base URL to calendar.scarriffle.com
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>