A block too short for the time line is too short for a second title line
too, so asking for two served a sliced one and handed the drag copy a
count it could not honour.
The viewport-sized parent capped a piece taller than one screen, so a
full-day column ended a scroll offset short of its own bottom. It also
reserved title lines from the source block's width, which for a block
sharing its column is half the copy's — the unused line took the time
range with it.
Wrapping at word boundaries brought the "…" back with it, which is the
half of #164 that was worth keeping: on a chip this narrow the ellipsis
costs a couple of readable characters. A wrapped title now ends at the
last whole word it could fit and clips there, spending none.
Only a single RTL line still needs the ellipsis — softWrap off clips to
the node's left edge, which in RTL is the string's end. Wrapping turns
softWrap on, which cuts the logical end in both directions, so more than
one line needs no exception.
Two events side by side leave a column well under a word wide, and the
36dp floor forced any block below it onto one clipped line however much
height it had — so the narrower the lane, the more of the title was lost.
The floor is gone; a title now takes every line its block affords.
Titles also wrap whole words again. #164 clipped the last line mid-glyph
so a narrow chip spent none of its few characters on an ellipsis, which
on a block with room to wrap reads worse than the ellipsis does:
"Farmers Market" over two lines beats "Farmer" / "s Marke".
The 2% pick-up scale ran from the middle, so the copy's top edge rose by
one percent of its height. On an hour that is a pixel or two; on a
multi-day piece it is several, and it grows as the piece does — the title
and range crept upward all the way through a drag along the tail, sheared
against the timeline's clip, and snapped back when the lift animated out
on drop. Anchored at the top edge it cannot move the text at all.
Also drops the text inset added in 038250a. It pinned a scrolled-off
piece's text to the visible edge, which the grid's own blocks don't do,
so the text shifted again when the copy handed back. Labelling the piece
with the most of itself in view is what fixed#267; the inset was not.
A multi-day event drags one piece per day, and a middle day's piece is a
full 24 hours — taller than the viewport, its top at a midnight scrolled
out of sight. The range went to the tallest piece by raw height, so it
was drawn at that top, off the timeline entirely, and the overlay draws
unclipped so the piece painted over the headers as well.
The copy is now clipped to the timeline, the range goes to the piece
with the most of itself in view, and a piece whose top has scrolled off
brings its text down to the visible edge and budgets from there.
The title is also served in full before the range: the hour gutter still
says where the copy sits, so the range is the half that can afford to go.
The copy always draws the range but was budgeting height for the title
first, so a block too short to have shown either — Day hands its whole
height to the title there, and passed that as the lines it drew — spilled
text past a chip nothing clips.
The copy now pays for the range first and gives the title what of its
lines still fit, and the blocks report the lines they actually drew.
The 2.dp text inset the three of them each spelled out is now a constant.
The floating copy hardcoded maxLines = 1 on its time label and its
title, so lifting a block whose text wrapped clipped it for as long as
it was held and snapped back on drop. The copy now measures the range
the same way the block does, and carries the block's title budget
through the drag.
Extracts that measurement into blockTimeLines, which Week and Day had
a copy of each.
Events waiting on your answer now read differently from the ones you have answered.
`EventInstance.isDeclined` becomes an `EventResponse` enum — `Going` / `Invited` / `Declined` — so there is one source of truth for the column; `isDeclined` stays as an extension property, so the surfaces that only care about that case are untouched. A tentative "maybe" counts as `Going`.
An unanswered invitation is drawn as an outline: the calendar's colour on the border and on the title, at regular weight, filled with the surface the chip sits on. The outline leaves open whichever edges the event runs past, so a bar carried across a week boundary stays one bar. The accent is harmonised even when soften-colours is off — raw mode exists so a filled container matches the sync source, and an outlined chip has no container, only coloured text, which needs its lightness pinned or a pale calendar goes unreadable.
Applied to the month chips, the week and day all-day bars, both timed blocks and the drag copy.
Declined events also sort last in the month view now: the timed chips, the split style's day pane, and the all-day bars, which pack into a lane below the ones you answered. A day that overflows drops the events you said no to first. `layoutAllDay` had to stop deciding lanes from how far right each one reaches and track the columns it holds instead, or packing declined last would waste a lane.
### Deviations from the issue
The issue asked for declined invitations to be outlined too, with the title crossed out. They keep their filled container instead: at chip size the strike-through needs something under it to read against, and on a hollow chip it was illegible. Declined is still distinguished — filled and struck through, against filled and plain — and the sort order is the extra separation it gained.
Not covered, and each needs its own mark rather than this one: the agenda rows, search results and the split view's day pane carry a 6dp colour stripe rather than a filled chip, and both widgets are Glance, which has no border modifier. All four still mark declined only.
Closes#230
Co-authored-by: Jean-Luc Makiola <business@jeanlucmakiola.de>
Reviewed-on: https://codeberg.org/jlmakiola/calendula/pulls/268
The medium-to-regular step is a no-op on Atkinson Hyperlegible, JetBrains
Mono and imported fonts, which ship no medium face — so the title takes
full ink against the time's 0.8 instead, and the four title sites share
the constant.
Also: the drag copy carries the time its source chip had rather than
deriving one with continuesLeft hardcoded false; the month grid takes
its zone from the state; times are formatted once per row and the chip
width measured once per grid; the chip geometry lives in one file; and
a screen reader hears the time whether or not the chip is wide enough
to draw it.
InlineField pinned KeyboardCapitalization.None for every field, which
overrode floret-kit's Sentences default. The override moves to location,
which is as often a URL as an address.
Closes#146
Long-pressing one of the three free-text fields in the event view puts the whole field on the clipboard.
| Field | Tap | Long-press |
| --- | --- | --- |
| Title | — | copy |
| Location | opens maps (unchanged) | copy |
| Description | links still open (unchanged) | copy |
Times, calendar, recurrence and attendees stay uncopyable. The confirmation snackbar only shows below API 33 — Android 13+ raises its own clipboard chip. A failed copy is reported on every API level.
First clipboard use in the app, so the helper lives in `ui/common` as a `FieldCopier` plus a `copyOnLongPress` modifier. A field with no tap action gets the gesture through `pointerInput` and merged `onLongClick` semantics rather than a `clickable` that would announce a tap it doesn't have.
Deviation worth naming: the description takes the gesture on its card rather than its text. A linkified URL consumes the press over its own glyphs, so long-pressing a URL copies nothing either way — the card at least adds the icon and padding as target. A description that is nothing but a link stays awkward to copy.
Closes#195
Co-authored-by: Jean-Luc Makiola <business@jeanlucmakiola.de>
Reviewed-on: https://codeberg.org/jlmakiola/calendula/pulls/263
### Part A — the title shortens instead of wrapping
`CalendarTitleButton` measures the full title against the width the bar hands
the title slot (`BoxWithConstraints` + `rememberTextMeasurer`) and falls back to
a shorter form when it does not fit. One pass, so no flicker, and it adapts on
its own to locale, font scale and to the pill being absent after #150.
- Month and week fall back to the three-letter month ("Sep", "Mär").
- Day drops the weekday instead ("Wed, 2 Sep 2027" → "2 Sep 2027") — there is no
shorter month form left there, `EEEdMMM` is already abbreviated.
- Either way the title now clamps to one line and ellipsises rather than growing
the bar to two lines.
The choice itself is a pure `titleFor(title, shortTitle, titleWidth,
availableWidth)`, so it is unit-tested rather than only reachable through a
composable. `formatCalendarTitle` gained no new behaviour, so the widget header
is untouched.
### Part B — the trailing edge, not the whole rhythm
`AppBarSpacing` holds the shared values: the side inset (also consumed by the
month grid, so the pill lines up with the content under it), the title's start
inset, and the two trailing insets — one measured to a container's background,
one to an icon button's glyph.
**Deviation from the issue:** the issue asks for one edge inset at *both* ends
and one gap between adjacent controls. Only the trailing edge and the title
inset are shared here. M3 derives both the title's position and the gap between
two adjacent action icons from the icon buttons' width, so putting the leading
edge and the inter-icon gaps on the same rhythm meant narrowing the icon buttons
from 48.dp to 32.dp — that reads cramped and shrinks the touch targets. The
leading edge and the icon gaps stay on M3's defaults. The title's 8.dp start
inset stays real padding rather than a negative offset, so the menu button's hit
box is intact.
The agenda range bar picks its end inset from whether the view switcher is
showing: aligned to the pill's background when it is, to the search icon's glyph
once #150 hides it.
Closes#165
Co-authored-by: Jean-Luc Makiola <business@jeanlucmakiola.de>
Reviewed-on: https://codeberg.org/jlmakiola/calendula/pulls/261
Release v2.19.4.
**Fixed**
- A deleted occurrence stays deleted through a series re-timing, and through backup/restore ([#225]).
- "Only this event" saves the edit on calendars that never synced, and on a row that doesn't recur it edits or deletes the event itself instead of filing an exception against nothing.
- A reminder left on the account's default is no longer read as a lead time of Calendula's own.
- The widgets turn the page at midnight; the re-arm backstop for the inexact rollover alarm is back, throttled to once per 15 minutes on calendar changes.
- An event running past midnight can be dragged by either half ([#253]).
- One event the provider refuses no longer costs the rest of the import, and an import cut short reports honestly how far it got.
- EXDATE zone prefixes are read in the bare `<zone>;` form the provider and DAVx5 actually write, not only the iCalendar `TZID=` spelling; a list holding a stamp we can't parse is kept whole rather than re-emitted stripped of its zone; RDATE travels with RRULE when the recurrence set is rewritten.
**Added**
- Belarusian, Hungarian and Slovak, from community translators on Weblate.
**Changed**
- Arabic, Polish and Simplified Chinese are complete; Czech grew, and Russian is now at about half.
Bumps `versionName` to 2.19.4, so merging this triggers the release pipeline.
Co-authored-by: Jean-Luc Makiola <business@jeanlucmakiola.de>
Reviewed-on: https://codeberg.org/jlmakiola/calendula/pulls/259
Calendula is published on Google Play, so the pipeline and the docs stop treating it as pending.
**README**
- Google Play badge next to F-Droid/Obtainium, linking the store page.
- Install table: the Play row goes from "Coming soon / —" to a real channel.
- The "coming soon" section becomes a live one with the store link; the closed-testing "testers wanted" call drops out. The note that Play signs with Google's key (so switching channels needs an uninstall) stays, and the trailing shared-signing-key line now flags Play as the exception.
**Play track**
- Release uploads default to `production` instead of `internal` — in the lane (`fastlane/Fastfile`) and in the workflow default (`.gitea/workflows/release.yaml`). Merging a bumped versionName to main is already the human gate, so the manual Console promotion only added delay.
- `docs/RELEASING.md` updated to match, including the escape hatches (`PLAY_TRACK=internal` to stage, `PLAY_RELEASE_STATUS=draft` to hold).
Note: if a `PLAY_TRACK` repo variable is set on Gitea it still wins over the new default — needs checking there.
No issue for this one.
Co-authored-by: Jean-Luc Makiola <business@jeanlucmakiola.de>
Reviewed-on: https://codeberg.org/jlmakiola/calendula/pulls/244