<!--
Thanks for contributing to Calendula!
Please skim CONTRIBUTING.md if you haven't:
https://codeberg.org/jlmakiola/calendula/src/branch/main/CONTRIBUTING.md
Two things it's easy to get wrong:
• Features need a discussed issue first. An undiscussed feature PR may be
closed unmerged even when the code is good.
• Target the release branch for your issue's milestone (milestone 2.18.0 →
release/v2.18.0), not main. If you targeted main, just say so below and it
will be retargeted.
-->
Targeted main, issue has no milestone
No entry needed in CHANGELOG.md (?)
### What this changes
This fixes the tapping animation for week numbers, day cells in month split view and the handle bar to expand the split view
week numbers:
Because Compose draws the click ripple indication at the level clickable is placed in the modifier chain, the ripple:
1. Extended into the cell padding area (making it larger than the rounded box).
2. Was unclipped, resulting in sharp square corners instead of rounded corners.
day cells:
The tap/selection ripple animation on day cells in the split month view is now constrained to CELL_SHAPE.
handle bar:
Tapping on the handle bar will now trigger the expand/collapse action cleanly without showing a tap ripple effect across the bar.
### Why
Closes#413
<!-- Closes#123: link the issue this implements or fixes. -->
### How it was tested
./gradlew lint test assembleDebug
<!--
Which of these ran green, and anything you exercised by hand. On-device notes
are especially useful for UI changes.
./gradlew lint test assembleDebug
python3 scripts/check_translations.py
-->
### Checklist
- [x] There's an issue for this, and (for a feature) it got a go-ahead
- [x] Targeting the release branch for that issue's milestone (or `main`, noted above)
- [x] `./gradlew lint test assembleDebug` passes locally
- [x] No `values-*/strings.xml` touched (Weblate owns those; new English strings in `values/` are fine)
- [x] `CHANGELOG.md` updated under `## [Unreleased]`, if the change is user-visible
- [x] No planning or design documents committed
Co-authored-by: Jean-Luc Makiola <jlmakiola@noreply.codeberg.org>
Co-authored-by: Weblate <noreply-mt-weblate@weblate.org>
Co-authored-by: Sup99999 <alelmnour1o234@proton.me>
Co-authored-by: VinCap <vcapaldo@ik.me>
Co-authored-by: N Donkersloot <n@donkerslootjes.nl>
Co-authored-by: MaxrregMustermann <MaxrregMustermann@airmail.cc>
Co-authored-by: Dmitri Gabinski <dkgabi@gmail.com>
Co-authored-by: James Trivette <trivette@atlas.cz>
Co-authored-by: Admin <business@jeanlucmakiola.de>
Co-authored-by: mirai yamada <weblate@197911mi.anonaddy.com>
Reviewed-on: https://codeberg.org/jlmakiola/calendula/pulls/414
Adds Liberapay as a second donation option next to Ko-fi.
- Settings: "Support development" now opens a full-screen picker with Ko-fi (one-off tips) and Liberapay (recurring donations) instead of linking straight to Ko-fi.
- README: Liberapay badge next to the Ko-fi badge, plus a short Support section.
No issue for this one.
Co-authored-by: Jean-Luc Makiola <business@jeanlucmakiola.de>
Reviewed-on: https://codeberg.org/jlmakiola/calendula/pulls/430
Fixes from reviewing the 2.23.0 release branch, plus a second pass over those fixes.
- Moved recurring exceptions keep their colour when they change calendar, instead of losing their colour key.
- An external request (widget, edit intent, share) now resets the form it replaces. Forms reset only after their slide-out ends, so they never go blank on the way out.
- Import, duplicate and picker state survive rotation, and recreating the activity no longer replays the launch intent.
- A shared .ics attachment opens the import, even when the email app adds a subject line as text. Plain text still opens a new event.
- Stale import state no longer reopens the previous import's form.
- The theme and dynamic colour are cached so a cold start draws the right colours from the first frame.
- Write-permission prompts (tap to edit, delete, duplicate, selection actions, week drafts) keep their pending action across rotation, and tap to edit falls back to details when the prompt is refused.
- Zone and date sources share one app-scoped receiver instead of one per collector.
- Scrolling back over an empty agenda past grows the window by at most a year per step, not exponentially.
- RRULE part names and the RRULE: prefix are matched case-insensitively on import.
- Search closes when another overlay takes over.
Closes#306, #178, #387, #367
Co-authored-by: Jean-Luc Makiola <business@jeanlucmakiola.de>
Reviewed-on: https://codeberg.org/jlmakiola/calendula/pulls/428
Step 8 of the simplification review: the three behaviour-sensitive merges. Each lands behind a characterization test written and passed against the old code first.
**Month row seating**
- `MonthWeek.seating(col, laneCap)` is the one source for which event sits in which lane and what overflows. `laneEvents`, `chipAt`, `overflowEvents` and MonthWeekRow's inline copy all build on it, so dots, bars and pills can't drift apart (#53).
- MonthWeekRow computes the seating once per row (`remember(week, laneCap)`) instead of re-filtering every column on each recomposition.
- `OverflowDots.total` and `shownLanes` are gone; they always equalled `events.size` and `laneCap`.
- MonthSeatingTest pins the four old copies against each other over 400 generated weeks × lane caps 0–6; they agreed everywhere, and `seating()` matches them.
**Timeline resize**
- TimelineDragController's eight loose `resize*` fields are one `ResizeSession?`, so cancel clears it in one assignment. Moves mutate it in place; no per-frame allocation. None of the fields were Compose state.
- Only difference: a resize starting without the previous one ending used to inherit the old held slot; it now starts clean.
**Timeline paging**
- Week, multi-day and day page through one `DaysGrid` with `weeks(firstDay)`, `days` and `startingAt(date, count)` factories; the PageGrid interface, WeekGrid and DayGrid are gone. Page numbers and grid equality (used by pager keys) are unchanged; PageNumberingTest pins them.
- jumpToDate no longer detects the multi-day view by class; it re-frames when the target would open mid-page, which a test checks against the old rule for every in-range date.
Not yet checked on a device. Worth a look: month bars/pills/+N dots on crowded days in paged and split styles plus the split morph, chip taps and month drag; resize of both edges incl. across midnight, cancel then plain drag, RTL; paging, view switching, jump to date/today, week-start and multi-day count changes.
Co-authored-by: Jean-Luc Makiola <business@jeanlucmakiola.de>
Reviewed-on: https://codeberg.org/jlmakiola/calendula/pulls/418
Step 7 of the simplification review: structure changes in the three biggest UI files. No intended visible change, except one fix noted under the host.
**Editor** (EventEditScreen)
- Pass-through wrappers removed: InlineField → floret InlineTextField, SaveScopeDialog → RecurringScopeDialog, REMINDER_QUICK_PICKS → REMINDER_PRESETS, reminderLabel/reminderLeadText → reminderLeadTimeLabel, currentDetailLocale → currentLocale.
- One PickerCardBody draws the time zone, recurrence, visibility and colour cards.
- The seven saveable showXPicker booleans are one saveable `EditSheet?` slot, so two sheets can't be open at once. EventEditContent goes from ~770 to ~250 lines; each card section and the sheet host are their own composables.
- Not done: GroupedSurface on the grouped form cards, since it adds a visible press-morph.
**Timeline** (WeekScreen, TimelineDrag)
- A remembered TimelineActions holder (scroll state, drag controller, five callbacks) replaces the seven values threaded through nine composables. Callbacks go through rememberUpdatedState, so the holder stays stable and pages don't recompose.
- TimelineDragController shares pieces(), settle(), column() (the RTL flip, was 5 copies) and snapToSlot() between move and resize. No extra per-frame work.
**Calendar host** (CalendarHost, RootScreen, MainActivity, view models)
- SlideOverlay replaces seven copies of the overlay enter/exit block; rememberHeld replaces the hand-paired held keys; one saveable CreateRequest replaces three create fields, and the widget create path calls openCreate.
- An ExternalRequests holder built in MainActivity replaces 13 parameters declared twice in RootScreen and CalendarHost.
- Month, week and agenda view models share one rollover-following today flow. CalendarHost keeps its own, which survives recreation.
- Fix: after rotation or process death with the detail or edit overlay open, closing it slid out a blank screen, because the held key was only set by in-app opens. It now keeps its content through the slide-out, as ARCHITECTURE.md describes.
Not yet checked on a device. Worth a look before release: open/close of detail, edit and create from every entry point (tap, notification, widget, QS tile, ACTION_INSERT/EDIT, .ics), rotation with an overlay open, every editor picker incl. rotation, and timeline move/resize/drag-to-create incl. RTL and after pinch-zoom.
Co-authored-by: Jean-Luc Makiola <business@jeanlucmakiola.de>
Reviewed-on: https://codeberg.org/jlmakiola/calendula/pulls/417
Step 6 of the simplification review: mechanical helper extractions, one commit per group. Widget code is untouched.
**Data layer** (CalendarDataSource and the mappers)
- `updateEventRow`/`eventUri` replace the seven "update, throw if no row" copies and the loose `withAppendedId` spellings.
- `SeriesRow.timeColumns()` / `exdateValues()` define the DTSTART/RRULE/DURATION/TZ/ALL_DAY set once for truncate, split, detach, delete and exception copy.
- One `insertReminderRow` for the four reminder loops, `insertLocalCalendar` for local and managed creates, `applyManagedCalendarReminders` iterates `queryManagedEvents`, `reconcileAttendees` reuses `insertAttendees`, plus `mapRows`, `instancesUri`, `mintUid`, shared `eventColorColumns` and `switchedOffCalendarIds`.
- The read-side mappers share `ColumnReader` helpers (end time, status, all-day UTC date); each keeps its own end-time clamp. `importDuration` goes through `rfc2445Duration` with identical output. The `eventColorPalette` data-source member is gone; the repository calls the pure function.
- One behaviour difference: a provider *exception* while inserting a reminder row was only swallowed on import. It is now logged and skipped on every path (create, update, reconcile, move), as the existing comments already describe it, so it no longer fails the event write.
**Domain**
- `BasicFormat.kt` replaces the four `%04d…` formatters and three substring parsers (ICS writer/parser, Recurrence, RecurrenceText). Parsing stays as lenient as before.
- `rruleParts()` for the RRULE tokenising in five places; each caller keeps its own malformed-part handling. `rruleTruncatedAt` keeps its own tokeniser since it preserves garbage parts verbatim.
- `allDayFormTimes` and `normalizedRrule` for the three form-prefill copies, `zoneOrNull` for the eight zone guards, EventSearch orders months by `YearMonth`, IcsParser uses duration arithmetic and `firstOrNull`.
- FREQ matching no longer depends on the device locale (a lowercase rule failed to parse on Turkish-locale phones).
- Skipped: the contact-date parser rewrite (tests don't pin enough inputs) and the writer-side enum tables (their exhaustive `when` is a useful compile check).
**UI / view models**
- `dayRange(first, last, zone)` and one `startOfWeek` replace four start..end-of-day helpers and two identical week-start functions.
- `orUnnamed()` replaces 17 `ifBlank { calendar_unnamed }` spellings; a shared `CalendarVisibilitySwitch` and `CalendarManagerRow`.
- Filter uses `CalendarAccountGroup` plus a hidden-id set instead of its own copy of the grouping model; the grouping test moved to `AccountGroupsTest`.
- `agendaDays` is shared by the Agenda screen and the split-month pane.
- kotlinx-datetime's `yearMonth`, `firstDay`/`lastDay` and java.time converters replace about 20 hand-written bridges; one `formatTimeOfDay` for agenda rows and month chips.
- No visible UI change.
Net about −240 lines.
Co-authored-by: Jean-Luc Makiola <business@jeanlucmakiola.de>
Reviewed-on: https://codeberg.org/jlmakiola/calendula/pulls/416
Step 5 of the simplification review (preferences pipeline). Needs floret-kit#15 merged first; the submodule pin (3b376ac) is on that PR's branch, not on floret main yet.
- **SettingsPrefs, CalendarPrefs, ReminderStatePrefs** read and write through floret core-prefs (new dependency). Every setting is one `Pref` in a new `SettingsKeys` object, so each flow and setter is a single line. Stored key names, defaults and clamping are unchanged; the old and new key lists diff clean. Flows are now distinct-until-changed, so an unrelated key's write no longer re-emits them.
- **ThemeMode** is floret's (same values, same `theme_mode`/`dynamic_color` keys). MainActivity uses floret's `isDark`/`isPureBlack` instead of its two hand-written `when`s.
- **Onboarding flags**: the eight per-step flow/setter pairs are one `OnboardingStep` → pref map, with `onboardingProgress` and `setOnboardingDone(step, done)`. Same key names. OnboardingViewModel loses its three carrier classes and the back-navigation `when`.
- **SettingsViewModel** builds `SettingsUiState` from one prefs snapshot instead of the four-level nested `combine` and its seven carrier classes; the font and special-dates states do the same. The 30 one-line setters use a `save {}` helper. Setters with side effects (widgets, scan, scheduler) keep their bodies; the widget refresh code is untouched.
Net about −550 lines. Unit tests and lint pass; the prefs tests now seed raw on-disk key names instead of the removed internal key constants.
Co-authored-by: Jean-Luc Makiola <business@jeanlucmakiola.de>
Reviewed-on: https://codeberg.org/jlmakiola/calendula/pulls/415
Stacked on #409 (its commit shows here until it merges). Bumps floret-kit to 5093e1c (floret main, floret-kit#14) and deletes the local copies of what moved there. Widget code untouched.
- `GroupedRow(checked)` for the check-mark rows; `GroupedSwitchRow` for 16 switch rows; `OptionRows` in the three settings preview pickers; `CustomExpandRow` for the three "Custom" picker rows
- `IconInfoCard` replaces `EditCard` and `DetailCard`; `GroupedHint` replaces `SettingsHint` / `HintText` / `PickerDescription`
- Onboarding steps use `OnboardingScaffold(headline, supporting)` and `OnboardingPrimaryButton`
- `LanguagePickerRow(header, predictiveBack)` replaces the local `LanguageRow`
- `relativeTimeAgo`, `ReminderOverride.minutesOrEmpty()`
- New `core-android` dependency: `goAsync { }` in the reminder receivers, `hasPermission` for all non-widget permission checks, `broadcastTicks` behind TodaySource / ZoneSource, `setWakeup` in the reminder and snooze alarms, `stateWhileSubscribed` for the 39 `WhileSubscribed(5_000)` sites
One visible change: the expand rows in Event form and Notification settings show floret's rotating chevron instead of swapping ExpandLess/ExpandMore.
Kept local on purpose: the Reliable delivery row (no selected highlight today), the Dynamic colour switch (isn't dimmed when unavailable), HiddenCalendarsStep's headline (needs its inset padding) and ViewStep's onboarding block (headlineSmall).
Co-authored-by: Jean-Luc Makiola <business@jeanlucmakiola.de>
Reviewed-on: https://codeberg.org/jlmakiola/calendula/pulls/410
Step 3 of the simplification review. Tests only, no production code changes.
- `TestEvents.kt`: defaulted `testEvent` / `allDayEvent` / `timedEvent` / `testDetail` builders replace the full `EventInstance(...)` / `EventDetail(...)` spellings and the per-file `allDay`/`timed` helpers. Helpers with test-specific titles, colours or zones stay as one-line wrappers, so every built event is unchanged.
- `TestRepositories.kt`: `testDataStore` / `testPrefs` / `testSettings` / `testRepository` plus a JUnit 5 `MainDispatcherExtension` replace the repeated DataStore, repository and setMain/resetMain wiring.
Same 1302 tests before and after; widget tests untouched.
Co-authored-by: Jean-Luc Makiola <business@jeanlucmakiola.de>
Reviewed-on: https://codeberg.org/jlmakiola/calendula/pulls/408
First step of the simplification review. No behaviour change.
- Unreachable paged-month path (`MonthContent` / `MonthPager` / `MonthPage`); the month branch is now scrolling vs split
- `ui/day/` and its test, `MonthViewModel.anchorPage`, `PagedTimelineViewModel.goToDate`, `SettingsPrefs.setQuickSwitchConfig`, `TimelineHours.span`
- `PermissionViewModel` / `PermissionUiState`, replaced by a saveable `denied` flag in `PermissionScreen`
- `ReminderNotifier.post` no longer returns a value nobody read
- 38 unused imports outside the widget code
Co-authored-by: Jean-Luc Makiola <business@jeanlucmakiola.de>
Reviewed-on: https://codeberg.org/jlmakiola/calendula/pulls/407
Moving a recurring event to another calendar re-inserted its exception rows under the new event. That event has no `_sync_id` yet, so the rows never attached: deleted occurrences came back, and edited ones showed twice (original plus the edited copy).
The move now does what delete and edit-this-occurrence already do for unsynced series: cancelled occurrences become EXDATE entries on the moved series, edited ones become EXDATE plus a standalone event on the target calendar. The standalone copies are rolled back with the rest if the move fails.
`buildCopiedExceptionValues` is gone with its tests, and `ExceptionRowSnapshot`/`ExceptionProjection` only read the columns still used.
Adds `MoveEventExceptionsTest` (instrumented), which fails on the old code and passes now.
Co-authored-by: Jean-Luc Makiola <business@jeanlucmakiola.de>
Reviewed-on: https://codeberg.org/jlmakiola/calendula/pulls/404
Feedback from #141: when resizing with the left hand, the finger covers both the event's own time and the hour-column badge.
While a selected event is moved or resized, the time under its title in the top bar now follows the gesture. It shows what letting go would save, using the same calculation as the drop (real duration for a move, the untouched edge kept for a resize, DST-safe), and stays on the saved times until the event leaves its starting slot. It only recalculates when the snapped slot changes.
Closes#141
Co-authored-by: Jean-Luc Makiola <business@jeanlucmakiola.de>
Reviewed-on: https://codeberg.org/jlmakiola/calendula/pulls/403
Feedback from the #141 beta: a duplicate opened the editor with the same title and a plain Save button, so it looked like editing the original.
The editor button now says Create for every new event (new, duplicate, shared text, .ics, long-press draft) and Save only when editing an existing event. The label is taken from how the editor was opened, so it doesn't flip while the form loads.
The other two feedback points (handle shape, start time under the finger) are left as they are, see the comment on #141.
Closes#141
Co-authored-by: Jean-Luc Makiola <business@jeanlucmakiola.de>
Reviewed-on: https://codeberg.org/jlmakiola/calendula/pulls/402
- Swiping between days in the split month pane now ticks, like pulling the pane open and closed already did.
- Dropping a moved or resized event gives a confirm buzz. It only fires on a real drop; putting an event back where it was, cancelling, or undoing stays silent.
- Fixes a #141 flicker: after a resize, the block dimmed briefly as the floating copy faded out. It now takes over at full opacity, as a block released in place already did.
Deviation from the issue: the original ask (haptics when swiping the split view up and down) shipped in 2.22.0, so this covers the remaining gaps from the analysis. Month and week paging stays silent on purpose, and settings switches are left for a later floret-kit change.
Closes#116
Co-authored-by: Jean-Luc Makiola <business@jeanlucmakiola.de>
Reviewed-on: https://codeberg.org/jlmakiola/calendula/pulls/401
Follow-up to #394, from two review passes on the tap-opens-editor change.
- The edit form's view model is reused between opens, so work still running for a closed form could land on the next one. Backing out of a slow load and tapping another event showed the old event, and Save wrote to it. A delete finishing after back closed the next editor. Results from a closed form are now dropped.
- Back is ignored while a save or delete runs. The write can't be called back, so the form waits for the result and then closes or reports the error.
- Unsaved changes are now measured against the form as it opened, defaults included, instead of a flag every setter had to set.
- Taps read the setting and whether the calendar takes writes when they happen, so a tap right after a cold start or a return to the app opens the right screen.
- Without write access, the first tap asks once. A refusal opens the details from then on, until the permission is granted.
- Pinning an event to the tapped occurrence lives in one helper (`EventDetail.atOccurrence`), used by the detail, edit and selection-bar paths.
Closes#178
Co-authored-by: Jean-Luc Makiola <business@jeanlucmakiola.de>
Reviewed-on: https://codeberg.org/jlmakiola/calendula/pulls/397
Settings → Appearance → Theme gets a fourth option, Black, next to System, Light and Dark. It's the dark theme with pure black backgrounds, and it works with Material You as well as Calendula's own palette.
- The month cells and the hour grid are `surfaceContainer` tiles on a `surface` ground, so making every container black would erase the grid lines. floret-kit's `toPureBlack()` sets the ground to #000 and scales the containers down to about #13, #1A, #25 and #30. The tiles stay a shade above black, and sheets and menus still stand out from the page.
- Black always means dark. It's an option in the theme picker, so it doesn't follow the system's light/dark setting.
- Event colours read `isSystemInDarkTheme()` in 15 places, so an in-app Dark or Black on a phone in light mode got light-theme tints. They now read `LocalFloretDarkTheme`, the theme's own decision. The bug also affected Dark, so the changelog has a Fixed entry for it.
- The window backdrop came from the night resource qualifier, so an in-app theme flashed the system's light or dark background before the first frame. `WindowThemeCache` keeps a SharedPreferences copy of the theme choice, and `onCreate` reads it to set the backdrop and bar icons. The copy is written when the stored setting loads, so the first cold start after updating still shows the old backdrop.
- Widgets keep their current themes. They get their theme work in the next milestone.
Needs jlmakiola/floret-kit#13, which is merged. The submodule is pinned to its merge commit on floret-kit `main` (`b1905da`).
Tested with new unit tests for the transform and for saving and reloading `BLACK`, and by hand on the emulator.
Closes#181
Co-authored-by: Jean-Luc Makiola <business@jeanlucmakiola.de>
Reviewed-on: https://codeberg.org/jlmakiola/calendula/pulls/399
Event descriptions from Exchange/Outlook and Google Calendar are often HTML. The detail view now renders them instead of showing the raw markup.
- A new converter in `domain/HtmlText.kt` turns HTML into text plus styles and links. It only kicks in when the description has real tags, so plain text with `<3` or `<https://…>` is left alone.
- Kept: bold, italic, underline, strikethrough, super/subscript, monospace, line breaks, paragraphs, lists (bullets, numbers, `start`, nesting) and `<a href>` links (http(s), mailto, tel). Dropped: fonts, sizes, colours, images, and `<head>`/`<style>`/`<script>`/comments.
- Each table row becomes one line with its cells joined by " · ", and empty spacer cells disappear. There's no real grid yet; one can go on top later if invites need it.
- URL and phone autolinking runs on the rendered text and skips text that's already a link.
- Long-press copy gives the rendered text, with each link's address in brackets after it.
- Search matches the rendered text and link addresses, and snippets show clean text.
- The edit form still shows the raw HTML, as discussed in the issue, so saving never breaks the formatting.
The issue suggested a settings toggle that strips tags. Following the discussion in the comments, this renders HTML automatically instead, with no setting.
Known limit: the SQL pre-filter in search still runs on the raw column. A word that's stored as an entity (`R&D`) or split by tags (`<b>Kick</b>off`) isn't found.
I wrote my own parser instead of using `AnnotatedString.fromHtml`, because that goes through `android.text.Html`, which is stubbed in JVM unit tests, and search lives in the Compose-free domain layer.
Closes#271
Shared text, or text picked with the new "Create event" entry in the selection menu, opens the new-event form prefilled. The share's subject becomes the title, or the first line if there is no subject, and the rest of the text goes into the description. Start and length use the usual defaults (next full hour, default duration). Calendula doesn't try to read a date out of the text.
Differences from the plan in the issue comment:
- The line used as the title is left out of the description, so it doesn't show up twice.
- Titles are capped at 255 characters. If a title gets cut, the full text stays in the description.
- Sharing a plain-text file no longer sends it to the .ics importer.
- A saved or dismissed prefilled form no longer reopens after rotating the phone. This also fixes it for "add to calendar" requests from other apps.
- If a second request comes in while a prefilled form is open, it replaces the form when nothing was changed. Otherwise the save / discard / keep editing prompt comes first.
- If Calendula was started just for the request, closing or saving the form takes you back to the app you came from.
Closes#306
Co-authored-by: Jean-Luc Makiola <business@jeanlucmakiola.de>
Reviewed-on: https://codeberg.org/jlmakiola/calendula/pulls/396
On the day the clocks change, the day, 3-day and week views placed events by the time elapsed since midnight, so every event after the change sat an hour off. A 14:45 event on 25 October showed at 15:45. Blocks are now placed by wall clock.
When the clocks go back, 02:00 to 03:00 happens twice, and a wall-clock time alone can't say which of the two it means. This branch keeps track of it:
- The event form holds each time as a `FormTime`, the wall clock plus its UTC offset when that time is in the repeated hour. Validation and saving read the real instants from it, so a span ending in the repeated hour is valid and switching between the two passes is saved.
- A time picked in the form or dragged with a resize handle goes back to the event's original pass when it's set back to its original time. Otherwise it moves in the direction it was moved, stays after the start, and takes a pass the grid can draw where it was dropped.
- A drop onto a repeated time lands on the first pass, unless the event was already on the second.
- A block that runs into the repeated hour from before it is drawn to 03:00, and its label shows its real end.
- The editor, the event details and the pinned zone's "local time" line add the zone abbreviation (CEST or CET) to a time in the repeated hour.
- A recurring series that starts in the repeated hour keeps its pass when it's edited.
Also in this PR:
- `ZoneSource`, provided through Hilt, gives the timeline, the event form and reschedules the device zone. The timeline lays its pages out again when the zone changes.
- A drafted block (#153) opens the form at its exact start and end.
- A day move onto the spring-forward day keeps its wall-clock start, so moving a weekly series there is still offered for all events.
## Testing
The 1,258 unit tests pass. The instrumented suite has the same two failures as `release/v2.23.0` (`CalendarRepositorySmokeTest` and `PermissionScreenTest`), so nothing new.
I also checked it by hand on an emulator in Europe/Berlin, with the clock moved to each DST day:
- 25 October: the event from the issue (14:45, Europe/Brussels) is drawn at 14:45. A run from 02:10 CEST to 02:20 CET is drawn to 03:00 and labelled with its real end.
- Pulling the end of a 02:30 to 03:30 CET event up to 02:45 saves 02:45 CET, after its start.
- Pulling the end of a 01:00 CEST to 04:00 CET event up to 02:15 saves 02:15 CEST, and the block stays where it was dropped.
- 29 March: a 01:00 to 04:00 run and a 14:00 event both sit at their own times.
Picking times in the form and moving a series in the month view are covered by unit tests only.
Closes#387
Co-authored-by: Jean-Luc Makiola <business@jeanlucmakiola.de>
Reviewed-on: https://codeberg.org/jlmakiola/calendula/pulls/393
Phone numbers in an event's title, location and description are now links. Tapping one opens the dialer with the number filled in. Nothing is called until you press call, so the app needs no new permission.
- Android's own matcher finds the numbers (Linkify, backed by libphonenumber and the SIM's region). Its loose setting links too much in event text, so a cleanup step drops meeting IDs and PINs after a label, dates, dotted times like "9.30 – 12.00", amounts, anything under 7 digits and digits inside URLs.
- "+49 (0)151 …" dials without the (0). An extension ("ext. 567") becomes a pause, and a Zoom-style dial-in suffix (",,850…#") is kept.
- Detection runs in the detail ViewModel while the event loads, so the links are there on the first frame and survive rotation.
- Long-pressing a field copies it even when the press lands on a link. Before, the link swallowed the press.
- PhoneLinksTest covers the cleanup on the JVM. PhoneLinksPlatformTest (androidTest) runs the real matcher and passes on an emulator with a US SIM.
Known limit: on a device without a SIM (Wi-Fi tablets, some emulators) Android gives the matcher no region, so only numbers starting with + are linked.
Closes#339
Co-authored-by: Jean-Luc Makiola <business@jeanlucmakiola.de>
Reviewed-on: https://codeberg.org/jlmakiola/calendula/pulls/395
New setting under Settings → Views → Events: "Tapping an event opens" Details (default) or Edit form. The long-press toggle moves into the same group.
- Taps in the views and in search open the editor directly for events in writable calendars. Read-only events, reminder notifications and widgets always open the details.
- Installs without write access are asked for it on the tap; refusing opens the details instead.
- The editor gets a back guard: with unsaved changes it asks to save, discard or keep editing; otherwise it closes silently.
- The editor gets Duplicate, Share and Delete in an overflow menu, so skipping the details doesn't lose them.
- If the event can't be loaded, the details screen opens and shows why.
- Each editor overlay (create, edit, duplicate/import) now has its own ViewModel. Duplicating from an edited form previously reused the edit form's state.
- Deletes from the details screen, the editor and the selection bar share one error mapping. Deleting an event that's already gone counts as deleted in the details screen and the editor.
Deviation from the issue: it asked for back to save automatically. Following the discussion in the comments, auto-save isn't in this PR. Back asks only when something actually changed, and Details stays the default.
Closes#178
Co-authored-by: Jean-Luc Makiola <business@jeanlucmakiola.de>
Reviewed-on: https://codeberg.org/jlmakiola/calendula/pulls/394
Long press on free time in the day, 3-day and week views drops a block of the default event length there. It can be moved while still held, or dragged and resized with its handles after letting go. The tick opens the new-event form at that span; the ✕, a tap elsewhere or back discards it. The draft is a stand-in event that reuses the #141 selection, drag and resize machinery, so nothing is written until the form saves.
Also smooths the lift handover for all selected blocks: no one-frame dip when letting go, no bounce on the handles or on a block landing after a drop or resize, and the floating copy's time label now matches the block's so the text doesn't shift on pickup.
Deviations from the issue / agreed design:
- An unresized draft opens the form with the length it shows (the global default) rather than the per-calendar default, so the form never opens at a different time than the block on screen.
- A draft dropped on a day off screen snaps back instead of moving there, since nothing would draw it.
- The in-editor timeline picker from the issue isn't included; per the issue comments it isn't needed for now.
Also moves the Gitea release/beta jobs onto the light/gradle runners.
Closes#153
Co-authored-by: Jean-Luc Makiola <business@jeanlucmakiola.de>
Reviewed-on: https://codeberg.org/jlmakiola/calendula/pulls/390