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>