diff --git a/CHANGELOG.md b/CHANGELOG.md
index 19e5eeb..0d68afe 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+## [2.18.1] — 2026-08-06
+
+### Fixed
+- Tapping an empty slot in week or day view creates the event at the hour you
+ tapped, after the timeline has been zoomed. Pinching the day taller or shorter
+ — or changing **Hour height** in Settings — left the tap still being measured
+ against the old spacing, so the new event landed at some other hour entirely
+ ([#148]).
+
## [2.18.0] — 2026-07-31
### Added
@@ -1323,3 +1332,4 @@ automatically, with zero telemetry and no internet permission.
[#57]: https://codeberg.org/jlmakiola/calendula/issues/57
[#56]: https://codeberg.org/jlmakiola/calendula/issues/56
[#114]: https://codeberg.org/jlmakiola/calendula/issues/114
+[#148]: https://codeberg.org/jlmakiola/calendula/issues/148
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 595c6ce..e4fd84a 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -28,8 +28,8 @@ android {
// which builds this version and then creates the matching vX.Y.Z tag +
// release itself (versionCode is pinned to MAJOR*10000 + MINOR*100 +
// PATCH from versionName, e.g. 2.7.2 -> 20702). See docs/RELEASING.md.
- versionCode = 21800
- versionName = "2.18.0"
+ versionCode = 21801
+ versionName = "2.18.1"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/TimelineScale.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/TimelineScale.kt
index d514029..b236aef 100644
--- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/TimelineScale.kt
+++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/TimelineScale.kt
@@ -76,6 +76,18 @@ fun TimelineScale.hourHeight(viewportHeight: Dp): Dp = when (this) {
.coerceAtLeast(fillHourHeight(viewportHeight))
}
+/**
+ * The minute of the day a tap [offsetY] px down a timeline column means, snapped
+ * to the hour it landed in.
+ *
+ * [hourPx] must be the hour height the column is drawing at *now* — a tap
+ * detector that captured it when it was installed maps taps to a pre-pinch grid
+ * (#148). A zero or negative height (a viewport measured at nothing) has no grid
+ * to read, so it answers midnight rather than dividing by it.
+ */
+fun tappedMinuteOfDay(offsetY: Float, hourPx: Float): Int =
+ if (hourPx <= 0f) 0 else (offsetY / hourPx).toInt().coerceIn(0, 23) * 60
+
/**
* Shortest an event block may render, as a fraction of an hour. Blocks keep a
* floor so a 15-minute event stays tappable, but the floor scales with the hour
diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/day/DayScreen.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/day/DayScreen.kt
index 0c9098b..05c2ef5 100644
--- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/day/DayScreen.kt
+++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/day/DayScreen.kt
@@ -46,6 +46,7 @@ import androidx.compose.runtime.key
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.snapshotFlow
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
@@ -117,6 +118,7 @@ import de.jeanlucmakiola.calendula.ui.common.formatMinuteOfDay
import de.jeanlucmakiola.calendula.ui.common.GUTTER_WIDTH
import de.jeanlucmakiola.calendula.ui.common.HourGutter
import de.jeanlucmakiola.calendula.ui.common.hourSeparatorLines
+import de.jeanlucmakiola.calendula.ui.common.tappedMinuteOfDay
import de.jeanlucmakiola.calendula.ui.week.TimedBlock
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
@@ -623,6 +625,12 @@ private fun DayColumnCard(
LaunchedEffect(blocks, dragController.settling) {
dragController.noteGrid(date, blocks)
}
+ // The tap detector outlives the composition that installed it — a pinch or a
+ // Settings change moves the hour height without restarting it — so it reads
+ // the height and the callback through state handles instead of capturing
+ // them, or taps land on the scale the column had before the zoom (#148).
+ val currentHourPx = rememberUpdatedState(hourPx)
+ val currentOnCreateAt = rememberUpdatedState(onCreateAt)
Card(
// Plain rectangular column — the soft corners come from the outer
// rounded scroll viewport, so inner rounding would look odd at the edges.
@@ -643,8 +651,10 @@ private fun DayColumnCard(
// only fires on the column background. Snaps to the tapped hour.
.pointerInput(date) {
detectTapGestures { offset ->
- val hour = (offset.y / hourPx).toInt().coerceIn(0, 23)
- onCreateAt(date, hour * 60)
+ currentOnCreateAt.value(
+ date,
+ tappedMinuteOfDay(offset.y, currentHourPx.value),
+ )
}
},
) {
diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/week/WeekScreen.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/week/WeekScreen.kt
index a025355..b4debbe 100644
--- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/week/WeekScreen.kt
+++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/week/WeekScreen.kt
@@ -52,6 +52,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.snapshotFlow
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
@@ -128,6 +129,7 @@ import de.jeanlucmakiola.calendula.ui.common.GUTTER_CONTENT_START_INSET
import de.jeanlucmakiola.calendula.ui.common.GUTTER_WIDTH
import de.jeanlucmakiola.calendula.ui.common.HourGutter
import de.jeanlucmakiola.calendula.ui.common.hourSeparatorLines
+import de.jeanlucmakiola.calendula.ui.common.tappedMinuteOfDay
import de.jeanlucmakiola.calendula.ui.common.rememberCalendarSlideSpec
import de.jeanlucmakiola.calendula.ui.common.next
import de.jeanlucmakiola.floret.time.isoWeekNumber
@@ -771,6 +773,12 @@ private fun DayColumnCard(
LaunchedEffect(blocks, dragController.settling) {
dragController.noteGrid(date, blocks)
}
+ // The tap detector outlives the composition that installed it — a pinch or a
+ // Settings change moves the hour height without restarting it — so it reads
+ // the height and the callback through state handles instead of capturing
+ // them, or taps land on the scale the column had before the zoom (#148).
+ val currentHourPx = rememberUpdatedState(hourPx)
+ val currentOnCreateAt = rememberUpdatedState(onCreateAt)
Card(
// Plain rectangular columns — the soft corners come from the outer
// rounded scroll viewport, so inner rounding would look odd at the edges.
@@ -790,8 +798,10 @@ private fun DayColumnCard(
// blocks are consumed by their own handler first. Snaps to hour.
.pointerInput(date) {
detectTapGestures { offset ->
- val hour = (offset.y / hourPx).toInt().coerceIn(0, 23)
- onCreateAt(date, hour * 60)
+ currentOnCreateAt.value(
+ date,
+ tappedMinuteOfDay(offset.y, currentHourPx.value),
+ )
}
},
) {
diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml
index 3e41783..a429555 100644
--- a/app/src/main/res/values-es/strings.xml
+++ b/app/src/main/res/values-es/strings.xml
@@ -491,4 +491,5 @@
- Importando %d eventos
- Importando %d eventos
+ Armoniza los colores del calendario
diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml
new file mode 100644
index 0000000..92a7f63
--- /dev/null
+++ b/app/src/main/res/values-pt-rBR/strings.xml
@@ -0,0 +1,9 @@
+
+
+
+ - Minutes
+ - Hours
+ - Days
+ - Weeks
+
+
diff --git a/app/src/test/java/de/jeanlucmakiola/calendula/ui/common/TimelineScaleTest.kt b/app/src/test/java/de/jeanlucmakiola/calendula/ui/common/TimelineScaleTest.kt
index c78bcd4..15b1e19 100644
--- a/app/src/test/java/de/jeanlucmakiola/calendula/ui/common/TimelineScaleTest.kt
+++ b/app/src/test/java/de/jeanlucmakiola/calendula/ui/common/TimelineScaleTest.kt
@@ -126,4 +126,24 @@ class TimelineScaleTest {
assertThat(parseTimelineScale(stored)).isEqualTo(TimelineScale.Regular)
}
}
+
+ @Test
+ fun `a tap creates at the hour it landed in, at whatever scale`() {
+ // Same point on the same column reads as a different hour once the
+ // timeline has been pinched — which is the whole of #148: the tap has to
+ // be measured against the height the column is drawing at now.
+ assertThat(tappedMinuteOfDay(offsetY = 500f, hourPx = 100f)).isEqualTo(5 * 60)
+ assertThat(tappedMinuteOfDay(offsetY = 500f, hourPx = 50f)).isEqualTo(10 * 60)
+ // Within an hour it snaps back to that hour's start.
+ assertThat(tappedMinuteOfDay(offsetY = 599f, hourPx = 100f)).isEqualTo(5 * 60)
+ }
+
+ @Test
+ fun `a tap outside the day stays inside it`() {
+ assertThat(tappedMinuteOfDay(offsetY = -20f, hourPx = 56f)).isEqualTo(0)
+ assertThat(tappedMinuteOfDay(offsetY = 99_999f, hourPx = 56f)).isEqualTo(23 * 60)
+ // A viewport measured at nothing has no grid to read — midnight, not a
+ // division by zero.
+ assertThat(tappedMinuteOfDay(offsetY = 500f, hourPx = 0f)).isEqualTo(0)
+ }
}
diff --git a/fastlane/metadata/android/ar/changelogs/21801.txt b/fastlane/metadata/android/ar/changelogs/21801.txt
new file mode 100644
index 0000000..9fb5c70
--- /dev/null
+++ b/fastlane/metadata/android/ar/changelogs/21801.txt
@@ -0,0 +1,2 @@
+إصلاحات
+• عرض الأسبوع واليوم: النقر على مساحة فارغة يُنشئ الحدث في الساعة التي نقرت عليها، حتى بعد تكبير شريط الوقت أو تغيير ارتفاع الساعة.
diff --git a/fastlane/metadata/android/de-DE/changelogs/21801.txt b/fastlane/metadata/android/de-DE/changelogs/21801.txt
new file mode 100644
index 0000000..34add94
--- /dev/null
+++ b/fastlane/metadata/android/de-DE/changelogs/21801.txt
@@ -0,0 +1,2 @@
+Behoben
+• Wochen- und Tagesansicht: Ein Tippen auf einen freien Bereich legt den Termin jetzt zur angetippten Uhrzeit an – auch nachdem die Zeitleiste gezoomt oder die Stundenhöhe geändert wurde.
diff --git a/fastlane/metadata/android/en-GB/changelogs/21801.txt b/fastlane/metadata/android/en-GB/changelogs/21801.txt
new file mode 100644
index 0000000..e8f6b85
--- /dev/null
+++ b/fastlane/metadata/android/en-GB/changelogs/21801.txt
@@ -0,0 +1,2 @@
+Fixed
+• Week and day view: tapping an empty slot now creates the event at the hour you tapped, even after zooming the timeline or changing the hour height.
diff --git a/fastlane/metadata/android/en-US/changelogs/21801.txt b/fastlane/metadata/android/en-US/changelogs/21801.txt
new file mode 100644
index 0000000..e8f6b85
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/21801.txt
@@ -0,0 +1,2 @@
+Fixed
+• Week and day view: tapping an empty slot now creates the event at the hour you tapped, even after zooming the timeline or changing the hour height.
diff --git a/fastlane/metadata/android/es-ES/changelogs/21801.txt b/fastlane/metadata/android/es-ES/changelogs/21801.txt
new file mode 100644
index 0000000..516da95
--- /dev/null
+++ b/fastlane/metadata/android/es-ES/changelogs/21801.txt
@@ -0,0 +1,2 @@
+Corregido
+• Vista de semana y día: al tocar un hueco libre, el evento se crea a la hora que has tocado, incluso después de ampliar la línea de tiempo o cambiar la altura de la hora.
diff --git a/fastlane/metadata/android/fr-FR/changelogs/21801.txt b/fastlane/metadata/android/fr-FR/changelogs/21801.txt
new file mode 100644
index 0000000..bf43334
--- /dev/null
+++ b/fastlane/metadata/android/fr-FR/changelogs/21801.txt
@@ -0,0 +1,2 @@
+Corrigé
+• Vues Semaine et Jour : appuyer sur un créneau vide crée désormais l'événement à l'heure touchée, même après avoir zoomé sur la grille ou modifié la hauteur d'une heure.
diff --git a/fastlane/metadata/android/it-IT/changelogs/21801.txt b/fastlane/metadata/android/it-IT/changelogs/21801.txt
new file mode 100644
index 0000000..38ba36f
--- /dev/null
+++ b/fastlane/metadata/android/it-IT/changelogs/21801.txt
@@ -0,0 +1,2 @@
+Corretto
+• Vista settimana e giorno: toccando uno spazio libero l'evento viene creato all'ora toccata, anche dopo aver ingrandito la linea temporale o cambiato l'altezza dell'ora.
diff --git a/fastlane/metadata/android/pl-PL/changelogs/21801.txt b/fastlane/metadata/android/pl-PL/changelogs/21801.txt
new file mode 100644
index 0000000..359c9a2
--- /dev/null
+++ b/fastlane/metadata/android/pl-PL/changelogs/21801.txt
@@ -0,0 +1,2 @@
+Poprawki
+• Widok tygodnia i dnia: dotknięcie pustego miejsca tworzy teraz wydarzenie o dotkniętej godzinie, także po powiększeniu osi czasu lub zmianie wysokości godziny.
diff --git a/fastlane/metadata/android/pt-PT/changelogs/21801.txt b/fastlane/metadata/android/pt-PT/changelogs/21801.txt
new file mode 100644
index 0000000..bfbb427
--- /dev/null
+++ b/fastlane/metadata/android/pt-PT/changelogs/21801.txt
@@ -0,0 +1,2 @@
+Corrigido
+• Vista de semana e de dia: tocar num espaço livre cria o evento à hora tocada, mesmo depois de ampliar a linha temporal ou alterar a altura da hora.
diff --git a/fastlane/metadata/android/ru-RU/changelogs/21801.txt b/fastlane/metadata/android/ru-RU/changelogs/21801.txt
new file mode 100644
index 0000000..6dd2e26
--- /dev/null
+++ b/fastlane/metadata/android/ru-RU/changelogs/21801.txt
@@ -0,0 +1,2 @@
+Исправлено
+• Вид недели и дня: нажатие на свободное место создаёт событие на выбранный час — в том числе после масштабирования шкалы времени или изменения высоты часа.
diff --git a/fastlane/metadata/android/zh-CN/changelogs/21801.txt b/fastlane/metadata/android/zh-CN/changelogs/21801.txt
new file mode 100644
index 0000000..c3ec928
--- /dev/null
+++ b/fastlane/metadata/android/zh-CN/changelogs/21801.txt
@@ -0,0 +1,2 @@
+修复
+• 周视图和日视图:点按空白处会在所点的时间创建事件,即使在缩放时间轴或更改小时高度之后也是如此。
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index aeb3573..8eeeb9b 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,7 +1,7 @@
[versions]
agp = "9.2.1"
kotlin = "2.3.21"
-ksp = "2.3.9"
+ksp = "2.3.10"
hilt = "2.59.2"
coreKtx = "1.19.0"
appcompat = "1.7.1"
@@ -19,15 +19,15 @@ truth = "1.4.5"
androidxJunit = "1.3.0"
espressoCore = "3.7.0"
kotlinxDatetime = "0.7.0"
-kotlinxCoroutines = "1.10.2"
+kotlinxCoroutines = "1.11.0"
turbine = "1.2.1"
-hiltNavigationCompose = "1.3.0"
+hiltNavigationCompose = "1.4.0"
lifecycleCompose = "2.10.0"
androidxTestRules = "1.7.0"
# Glance: 1.1.1 is the latest stable (1.2.0 is still rc, 1.3.0 alpha).
glance = "1.1.1"
work = "2.10.1"
-documentfile = "1.0.1"
+documentfile = "1.1.0"
[libraries]
# AndroidX core