diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/MainActivity.kt b/app/src/main/java/de/jeanlucmakiola/calendula/MainActivity.kt index bc79ac0..840f09b 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/MainActivity.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/MainActivity.kt @@ -25,6 +25,7 @@ import de.jeanlucmakiola.calendula.data.crash.CrashReporter import de.jeanlucmakiola.calendula.data.prefs.ThemeMode import de.jeanlucmakiola.calendula.data.prefs.is24Hour import de.jeanlucmakiola.calendula.ui.RootScreen +import de.jeanlucmakiola.calendula.ui.common.LocalShowHourLines import de.jeanlucmakiola.calendula.ui.common.LocalUse24HourFormat import de.jeanlucmakiola.calendula.ui.WidgetNavRequest import de.jeanlucmakiola.calendula.ui.common.CalendarView @@ -97,7 +98,10 @@ class MainActivity : AppCompatActivity() { darkTheme = darkTheme, dynamicColor = settings.dynamicColor, ) { - CompositionLocalProvider(LocalUse24HourFormat provides use24Hour) { + CompositionLocalProvider( + LocalUse24HourFormat provides use24Hour, + LocalShowHourLines provides settings.showHourLines, + ) { RootScreen( modifier = Modifier.fillMaxSize(), requestedDetailKey = requestedDetailKey, diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/data/prefs/SettingsPrefs.kt b/app/src/main/java/de/jeanlucmakiola/calendula/data/prefs/SettingsPrefs.kt index 45c6c81..19ad74b 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/data/prefs/SettingsPrefs.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/data/prefs/SettingsPrefs.kt @@ -100,6 +100,18 @@ class SettingsPrefs @Inject constructor( store.edit { it[TIME_FORMAT_KEY] = pref.name } } + /** + * Whether the week/day timeline draws a faint separator line at each hour + * (v2.11). Defaults to OFF — the historical clean look; users opt in. + */ + val showHourLines: Flow = store.data.map { prefs -> + prefs[SHOW_HOUR_LINES_KEY] ?: false + } + + suspend fun setShowHourLines(enabled: Boolean) { + store.edit { it[SHOW_HOUR_LINES_KEY] = enabled } + } + /** * The calendar view the app opens on (M1). Defaults to [CalendarView.Week] — * the historical hard-coded startup view — so existing users see no change @@ -290,6 +302,7 @@ class SettingsPrefs @Inject constructor( internal val DYNAMIC_COLOR_KEY = booleanPreferencesKey("dynamic_color") internal val WEEK_START_KEY = stringPreferencesKey("week_start") internal val TIME_FORMAT_KEY = stringPreferencesKey("time_format") + internal val SHOW_HOUR_LINES_KEY = booleanPreferencesKey("show_hour_lines") internal val DEFAULT_VIEW_KEY = stringPreferencesKey("default_view") internal val FORM_FIELDS_KEY = stringPreferencesKey("event_form_default_fields") internal val REMINDERS_ENABLED_KEY = booleanPreferencesKey("reminders_enabled") diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/HourLines.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/HourLines.kt new file mode 100644 index 0000000..6ae2d71 --- /dev/null +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/HourLines.kt @@ -0,0 +1,38 @@ +package de.jeanlucmakiola.calendula.ui.common + +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color + +/** + * Whether the week/day timeline draws an hour separator line, from the + * `showHourLines` preference. Provided once at the app root (like + * [LocalUse24HourFormat]) so the timeline reads it without ViewModel plumbing. + * Defaults to off — the historical clean look. + */ +val LocalShowHourLines = staticCompositionLocalOf { false } + +/** + * Draw a faint separator line at the top of each hour (1..23) when [show] is + * true. Applied to a day column's content so each line sits over the column's + * background but beneath the event blocks. [hourHeightPx] is one hour's pixel + * height; [color] is resolved by the caller from the theme. + */ +fun Modifier.hourSeparatorLines(show: Boolean, hourHeightPx: Float, color: Color): Modifier = + if (!show) { + this + } else { + drawBehind { + for (hour in 1 until 24) { + val y = hour * hourHeightPx + drawLine( + color = color, + start = Offset(0f, y), + end = Offset(size.width, y), + strokeWidth = 1f, + ) + } + } + } 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 2d7e0ae..97cbdce 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 @@ -82,8 +82,10 @@ import de.jeanlucmakiola.calendula.ui.common.pastelize import de.jeanlucmakiola.calendula.ui.common.rememberCalendarSlideSpec import de.jeanlucmakiola.calendula.ui.common.currentLocale import de.jeanlucmakiola.calendula.ui.common.LocalUse24HourFormat +import de.jeanlucmakiola.calendula.ui.common.LocalShowHourLines import de.jeanlucmakiola.calendula.ui.common.formatHourLabel import de.jeanlucmakiola.calendula.ui.common.formatMinuteOfDay +import de.jeanlucmakiola.calendula.ui.common.hourSeparatorLines import de.jeanlucmakiola.calendula.ui.week.TimedBlock import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch @@ -539,6 +541,8 @@ private fun DayColumnCard( modifier: Modifier = Modifier, ) { val hourPx = with(LocalDensity.current) { HOUR_HEIGHT.toPx() } + val showHourLines = LocalShowHourLines.current + val hourLineColor = MaterialTheme.colorScheme.outlineVariant Card( // Plain rectangular column — the soft corners come from the outer // rounded scroll viewport, so inner rounding would look odd at the edges. @@ -551,6 +555,9 @@ private fun DayColumnCard( BoxWithConstraints( modifier = Modifier .fillMaxSize() + // Faint hour separators sit over the column background but under + // the event blocks (drawBehind paints before the children). + .hourSeparatorLines(showHourLines, hourPx, hourLineColor) // Tap an empty slot to create an event there. Taps on event // blocks are consumed by their own click handler first, so this // only fires on the column background. Snaps to the tapped hour. diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsScreen.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsScreen.kt index 62cdfa4..558aaa8 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsScreen.kt @@ -477,9 +477,21 @@ private fun AppearanceScreen( GroupedRow( title = stringResource(R.string.settings_time_format), summary = timeFormatLabel(state.timeFormat), - position = Position.Bottom, + position = Position.Middle, onClick = { showTimeFormat = true }, ) + GroupedRow( + title = stringResource(R.string.settings_hour_lines), + summary = stringResource(R.string.settings_hour_lines_summary), + position = Position.Bottom, + trailing = { + Switch( + checked = state.showHourLines, + onCheckedChange = viewModel::setShowHourLines, + ) + }, + onClick = { viewModel.setShowHourLines(!state.showHourLines) }, + ) } if (showTheme) { diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsUiState.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsUiState.kt index c4b9baf..12fde50 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsUiState.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsUiState.kt @@ -21,6 +21,8 @@ data class SettingsUiState( val weekStart: WeekStartPref = WeekStartPref.Auto, /** Clock convention for time labels (v2.11). AUTO follows the system setting. */ val timeFormat: TimeFormatPref = TimeFormatPref.AUTO, + /** Whether the week/day timeline draws an hour separator line (v2.11). */ + val showHourLines: Boolean = false, /** The calendar view the app opens on, and the home of the view back stack (M1). */ val defaultView: CalendarView = CalendarView.Week, /** Optional event-form fields shown by default (rest behind "more fields"). */ diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsViewModel.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsViewModel.kt index eea8c5c..e452eac 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsViewModel.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsViewModel.kt @@ -75,10 +75,12 @@ class SettingsViewModel @Inject constructor( ReminderOverrides(overrides, allDayOverrides, calendars) }, combine(prefs.defaultView, prefs.timeFormat) { view, timeFormat -> view to timeFormat }, - ) { base, defaults, overrides, viewAndTimeFormat -> + prefs.showHourLines, + ) { base, defaults, overrides, viewAndTimeFormat, showHourLines -> base.copy( defaultView = viewAndTimeFormat.first, timeFormat = viewAndTimeFormat.second, + showHourLines = showHourLines, allowColorOnUnsupportedCalendars = defaults.allowColor, defaultReminderMinutes = defaults.defaultReminder, defaultAllDayReminderMinutes = defaults.allDayReminder, @@ -124,6 +126,10 @@ class SettingsViewModel @Inject constructor( viewModelScope.launch { prefs.setTimeFormat(pref) } } + fun setShowHourLines(enabled: Boolean) { + viewModelScope.launch { prefs.setShowHourLines(enabled) } + } + fun setDefaultView(view: CalendarView) { viewModelScope.launch { prefs.setDefaultView(view) } } 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 1af83fa..63bbe74 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 @@ -84,8 +84,10 @@ import de.jeanlucmakiola.calendula.ui.common.rememberCalendarFadeSpec import de.jeanlucmakiola.calendula.ui.common.rememberReduceMotion import de.jeanlucmakiola.calendula.ui.common.currentLocale import de.jeanlucmakiola.calendula.ui.common.LocalUse24HourFormat +import de.jeanlucmakiola.calendula.ui.common.LocalShowHourLines import de.jeanlucmakiola.calendula.ui.common.formatHourLabel import de.jeanlucmakiola.calendula.ui.common.formatMinuteOfDay +import de.jeanlucmakiola.calendula.ui.common.hourSeparatorLines import de.jeanlucmakiola.calendula.ui.common.rememberCalendarSlideSpec import de.jeanlucmakiola.calendula.ui.common.next import de.jeanlucmakiola.calendula.ui.common.pastelize @@ -655,6 +657,8 @@ private fun DayColumnCard( modifier: Modifier = Modifier, ) { val hourPx = with(LocalDensity.current) { HOUR_HEIGHT.toPx() } + val showHourLines = LocalShowHourLines.current + val hourLineColor = MaterialTheme.colorScheme.outlineVariant Card( // Plain rectangular columns — the soft corners come from the outer // rounded scroll viewport, so inner rounding would look odd at the edges. @@ -667,6 +671,9 @@ private fun DayColumnCard( BoxWithConstraints( modifier = Modifier .fillMaxSize() + // Faint hour separators sit over the column background but under + // the event blocks (drawBehind paints before the children). + .hourSeparatorLines(showHourLines, hourPx, hourLineColor) // Tap an empty slot to create an event there; taps on event // blocks are consumed by their own handler first. Snaps to hour. .pointerInput(date) { diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 59f3460..5f700a3 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -280,6 +280,8 @@ Automatisch 12-Stunden (2:00 PM) 24-Stunden (14:00) + Stundenlinien + Trennlinie zu jeder vollen Stunde in Wochen- und Tagesansicht anzeigen Termin-Formular Standardmäßig angezeigte Felder — alles Weitere liegt hinter \"Weitere Felder\" Farben auf nicht unterstützten Kalendern erlauben diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b83d6f8..67ff790 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -273,6 +273,8 @@ Automatic 12-hour (2:00 PM) 24-hour (14:00) + Hour lines + Show a separator line at each hour in week and day view New event form Fields shown by default — everything else sits behind \"More fields\" Allow colors on unsupported calendars diff --git a/app/src/test/java/de/jeanlucmakiola/calendula/data/prefs/SettingsPrefsTest.kt b/app/src/test/java/de/jeanlucmakiola/calendula/data/prefs/SettingsPrefsTest.kt index 7d7d052..7f26267 100644 --- a/app/src/test/java/de/jeanlucmakiola/calendula/data/prefs/SettingsPrefsTest.kt +++ b/app/src/test/java/de/jeanlucmakiola/calendula/data/prefs/SettingsPrefsTest.kt @@ -73,6 +73,22 @@ class SettingsPrefsTest { assertThat(prefs.weekStart.first()).isEqualTo(WeekStartPref.Auto) } + @Test + fun `time format defaults to auto and round-trips`(@TempDir tempDir: Path) = runTest { + val prefs = SettingsPrefs(newDataStore(tempDir)) + assertThat(prefs.timeFormat.first()).isEqualTo(TimeFormatPref.AUTO) + prefs.setTimeFormat(TimeFormatPref.TWELVE_HOUR) + assertThat(prefs.timeFormat.first()).isEqualTo(TimeFormatPref.TWELVE_HOUR) + } + + @Test + fun `hour lines default off and round-trips`(@TempDir tempDir: Path) = runTest { + val prefs = SettingsPrefs(newDataStore(tempDir)) + assertThat(prefs.showHourLines.first()).isFalse() + prefs.setShowHourLines(true) + assertThat(prefs.showHourLines.first()).isTrue() + } + @Test fun `garbage stored enum falls back to default`(@TempDir tempDir: Path) = runTest { val store = newDataStore(tempDir)