feat(timeline): optional hour separator lines in week & day view (#5) #43

Merged
makiolaj merged 2 commits from feat/timeline-hour-lines into release/v2.11.0 2026-06-27 13:18:26 +00:00
9 changed files with 133 additions and 32 deletions
Showing only changes of commit f8efa4d3e4 - Show all commits

View File

@@ -5,6 +5,29 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [2.11.0] — 2026-06-27
### Added
- Start the week on any day. The **Week starts on** setting (Settings →
Appearance) now offers every weekday — not just Monday or Sunday — alongside
the automatic, locale-based default. Thanks to @zmaherdev for the suggestion
([#3]).
- Choose a 12- or 24-hour clock. A new **Time format** setting (Settings →
Appearance) lets you force a 12-hour (2:00 PM) or 24-hour (14:00) clock, or
follow the system setting automatically. It applies everywhere times appear —
the week and day timelines, agenda, event details, search and reminders — so
the format is now consistent across the whole app. Thanks to @zmaherdev for
the suggestion ([#6]).
- Optional hour lines in the timeline. A new **Hour lines** switch (Settings →
Appearance) draws a faint separator at each hour in the week and day views,
making it easier to see when events start and end. Off by default. Thanks to
@zmaherdev for the suggestion ([#5]).
- Limit how far ahead the agenda looks. New **Agenda range** and **Agenda widget
range** settings (Settings → Appearance) let the agenda screen and its widget
each show just today, the rest of this week, the rest of this month, a rolling
7 or 30 days, or a custom number of days. "This week" follows your week-start
preference. Thanks to @zmaherdev for the suggestion ([#4]).
## [2.10.0] — 2026-06-25
### Added
@@ -627,3 +650,7 @@ automatically, with zero telemetry and no internet permission.
[#1]: https://codeberg.org/jlmakiola/calendula/issues/1
[#2]: https://codeberg.org/jlmakiola/calendula/issues/2
[#3]: https://codeberg.org/jlmakiola/calendula/issues/3
[#4]: https://codeberg.org/jlmakiola/calendula/issues/4
[#5]: https://codeberg.org/jlmakiola/calendula/issues/5
[#6]: https://codeberg.org/jlmakiola/calendula/issues/6

View File

@@ -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 = 21000
versionName = "2.10.0"
versionCode = 21100
versionName = "2.11.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}

View File

@@ -19,8 +19,14 @@ import javax.inject.Singleton
/** Light/dark override. SYSTEM follows the device setting. */
enum class ThemeMode { SYSTEM, LIGHT, DARK }
/** Week-start override. AUTO derives the first day from the active locale. */
enum class WeekStartPref { AUTO, MONDAY, SUNDAY }
/**
* Week-start override. [Auto] derives the first day from the active locale;
* [Day] pins a specific weekday (any of the seven).
*/
sealed interface WeekStartPref {
data object Auto : WeekStartPref
data class Day(val day: DayOfWeek) : WeekStartPref
}
/**
* Clock convention for time-of-day labels. AUTO follows the device's 24-hour
@@ -39,14 +45,13 @@ fun TimeFormatPref.is24Hour(systemIs24Hour: Boolean): Boolean = when (this) {
}
/**
* Resolve the preference to a concrete first-day-of-week. AUTO reads the
* locale's convention (e.g. Monday in DE, Sunday in en-US).
* Resolve the preference to a concrete first-day-of-week. [WeekStartPref.Auto]
* reads the locale's convention (e.g. Monday in DE, Sunday in en-US).
*/
fun WeekStartPref.resolveFirstDay(locale: Locale): DayOfWeek = when (this) {
WeekStartPref.MONDAY -> DayOfWeek.MONDAY
WeekStartPref.SUNDAY -> DayOfWeek.SUNDAY
is WeekStartPref.Day -> day
// java.time.DayOfWeek.value is ISO 1..7 (Mon..Sun) — same numbering kotlinx uses.
WeekStartPref.AUTO -> DayOfWeek(WeekFields.of(locale).firstDayOfWeek.value)
WeekStartPref.Auto -> DayOfWeek(WeekFields.of(locale).firstDayOfWeek.value)
}
/**
@@ -71,7 +76,7 @@ class SettingsPrefs @Inject constructor(
}
val weekStart: Flow<WeekStartPref> = store.data.map { prefs ->
prefs[WEEK_START_KEY].toEnum(WeekStartPref.AUTO)
parseWeekStart(prefs[WEEK_START_KEY])
}
suspend fun setThemeMode(mode: ThemeMode) {
@@ -83,7 +88,7 @@ class SettingsPrefs @Inject constructor(
}
suspend fun setWeekStart(pref: WeekStartPref) {
store.edit { it[WEEK_START_KEY] = pref.name }
store.edit { it[WEEK_START_KEY] = pref.storageValue() }
}
/** Clock convention for time-of-day labels (v2.11). Defaults to AUTO (system). */
@@ -370,6 +375,25 @@ private fun MutableMap<Long, Int?>.applyOverride(
}
}
/** Sentinel stored for [WeekStartPref.Auto]; days store their [DayOfWeek.name]. */
private const val WEEK_START_AUTO = "AUTO"
private fun WeekStartPref.storageValue(): String = when (this) {
WeekStartPref.Auto -> WEEK_START_AUTO
is WeekStartPref.Day -> day.name
}
/**
* Parse the stored week-start value. "AUTO"/null/garbage → [WeekStartPref.Auto];
* a [DayOfWeek] name → [WeekStartPref.Day]. The legacy "MONDAY"/"SUNDAY" enum
* values migrate transparently, since both are valid day names.
*/
private fun parseWeekStart(stored: String?): WeekStartPref = when (stored) {
null, WEEK_START_AUTO -> WeekStartPref.Auto
else -> DayOfWeek.entries.firstOrNull { it.name == stored }
?.let { WeekStartPref.Day(it) } ?: WeekStartPref.Auto
}
private const val NONE = "none"
private const val ENTRY_SEP = ";"
private const val KEY_VALUE_SEP = "="

View File

@@ -106,7 +106,10 @@ import de.jeanlucmakiola.calendula.ui.common.TimePickerAlert
import de.jeanlucmakiola.calendula.ui.common.positionOf
import de.jeanlucmakiola.calendula.ui.common.reminderLeadTimeLabel
import de.jeanlucmakiola.calendula.ui.common.rememberCalendarSlideSpec
import de.jeanlucmakiola.calendula.ui.common.currentLocale
import kotlinx.datetime.DayOfWeek
import kotlinx.datetime.LocalTime
import java.time.format.TextStyle as JavaTextStyle
import java.util.Calendar
/** The settings sub-screens reached from the hub's category rows. */
@@ -504,7 +507,7 @@ private fun AppearanceScreen(
if (showWeekStart) {
OptionPicker(
title = stringResource(R.string.settings_week_start),
options = WeekStartPref.entries,
options = WEEK_START_OPTIONS,
selected = state.weekStart,
label = { weekStartLabel(it) },
onSelect = viewModel::setWeekStart,
@@ -1023,14 +1026,18 @@ private fun themeLabel(mode: ThemeMode): String = stringResource(
},
)
/** Picker options: "Follow system" first, then Monday…Sunday in ISO order. */
private val WEEK_START_OPTIONS: List<WeekStartPref> =
listOf(WeekStartPref.Auto) + DayOfWeek.entries.map { WeekStartPref.Day(it) }
@Composable
private fun weekStartLabel(pref: WeekStartPref): String = stringResource(
when (pref) {
WeekStartPref.AUTO -> R.string.settings_week_start_auto
WeekStartPref.MONDAY -> R.string.settings_week_start_monday
WeekStartPref.SUNDAY -> R.string.settings_week_start_sunday
},
)
private fun weekStartLabel(pref: WeekStartPref): String = when (pref) {
WeekStartPref.Auto -> stringResource(R.string.settings_week_start_auto)
// Localised full weekday name, so any of the seven days reads naturally
// without a per-day string resource.
is WeekStartPref.Day -> java.time.DayOfWeek.of(pref.day.ordinal + 1)
.getDisplayName(JavaTextStyle.FULL, currentLocale())
}
@Composable
private fun timeFormatLabel(pref: TimeFormatPref): String = stringResource(

View File

@@ -18,7 +18,7 @@ data class SettingsUiState(
val themeMode: ThemeMode = ThemeMode.SYSTEM,
val dynamicColor: Boolean = true,
val dynamicColorAvailable: Boolean = true,
val weekStart: WeekStartPref = WeekStartPref.AUTO,
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). */

View File

@@ -276,8 +276,6 @@
<string name="settings_dynamic_color_unavailable">Erfordert Android 12 oder neuer</string>
<string name="settings_week_start">Wochenstart</string>
<string name="settings_week_start_auto">Automatisch</string>
<string name="settings_week_start_monday">Montag</string>
<string name="settings_week_start_sunday">Sonntag</string>
<string name="settings_time_format">Zeitformat</string>
<string name="settings_time_format_auto">Automatisch</string>
<string name="settings_time_format_12h">12-Stunden (2:00 PM)</string>

View File

@@ -269,8 +269,6 @@
<string name="settings_dynamic_color_unavailable">Requires Android 12 or newer</string>
<string name="settings_week_start">Week starts on</string>
<string name="settings_week_start_auto">Automatic</string>
<string name="settings_week_start_monday">Monday</string>
<string name="settings_week_start_sunday">Sunday</string>
<string name="settings_time_format">Time format</string>
<string name="settings_time_format_auto">Automatic</string>
<string name="settings_time_format_12h">12-hour (2:00 PM)</string>

View File

@@ -25,7 +25,7 @@ class SettingsPrefsTest {
val prefs = SettingsPrefs(newDataStore(tempDir))
assertThat(prefs.themeMode.first()).isEqualTo(ThemeMode.SYSTEM)
assertThat(prefs.dynamicColor.first()).isTrue()
assertThat(prefs.weekStart.first()).isEqualTo(WeekStartPref.AUTO)
assertThat(prefs.weekStart.first()).isEqualTo(WeekStartPref.Auto)
}
@Test
@@ -43,10 +43,34 @@ class SettingsPrefsTest {
}
@Test
fun `week start round-trips`(@TempDir tempDir: Path) = runTest {
fun `week start round-trips any day`(@TempDir tempDir: Path) = runTest {
val prefs = SettingsPrefs(newDataStore(tempDir))
prefs.setWeekStart(WeekStartPref.SUNDAY)
assertThat(prefs.weekStart.first()).isEqualTo(WeekStartPref.SUNDAY)
prefs.setWeekStart(WeekStartPref.Day(DayOfWeek.SATURDAY))
assertThat(prefs.weekStart.first()).isEqualTo(WeekStartPref.Day(DayOfWeek.SATURDAY))
}
@Test
fun `legacy MONDAY week-start value migrates to Day`(@TempDir tempDir: Path) = runTest {
val store = newDataStore(tempDir)
val prefs = SettingsPrefs(store)
store.updateData { p ->
val m = p.toMutablePreferences()
m[SettingsPrefs.WEEK_START_KEY] = "MONDAY"
m
}
assertThat(prefs.weekStart.first()).isEqualTo(WeekStartPref.Day(DayOfWeek.MONDAY))
}
@Test
fun `garbage week-start value falls back to Auto`(@TempDir tempDir: Path) = runTest {
val store = newDataStore(tempDir)
val prefs = SettingsPrefs(store)
store.updateData { p ->
val m = p.toMutablePreferences()
m[SettingsPrefs.WEEK_START_KEY] = "FUNDAY"
m
}
assertThat(prefs.weekStart.first()).isEqualTo(WeekStartPref.Auto)
}
@Test
@@ -268,13 +292,15 @@ class SettingsPrefsTest {
@Test
fun `explicit week-start prefs resolve regardless of locale`() {
assertThat(WeekStartPref.MONDAY.resolveFirstDay(Locale.US)).isEqualTo(DayOfWeek.MONDAY)
assertThat(WeekStartPref.SUNDAY.resolveFirstDay(Locale.GERMANY)).isEqualTo(DayOfWeek.SUNDAY)
assertThat(WeekStartPref.Day(DayOfWeek.MONDAY).resolveFirstDay(Locale.US))
.isEqualTo(DayOfWeek.MONDAY)
assertThat(WeekStartPref.Day(DayOfWeek.SATURDAY).resolveFirstDay(Locale.GERMANY))
.isEqualTo(DayOfWeek.SATURDAY)
}
@Test
fun `auto week start follows the locale convention`() {
assertThat(WeekStartPref.AUTO.resolveFirstDay(Locale.GERMANY)).isEqualTo(DayOfWeek.MONDAY)
assertThat(WeekStartPref.AUTO.resolveFirstDay(Locale.US)).isEqualTo(DayOfWeek.SUNDAY)
assertThat(WeekStartPref.Auto.resolveFirstDay(Locale.GERMANY)).isEqualTo(DayOfWeek.MONDAY)
assertThat(WeekStartPref.Auto.resolveFirstDay(Locale.US)).isEqualTo(DayOfWeek.SUNDAY)
}
}

View File

@@ -0,0 +1,21 @@
### Added
- Start the week on any day. The **Week starts on** setting (Settings →
Appearance) now offers every weekday — not just Monday or Sunday — alongside
the automatic, locale-based default. Thanks to @zmaherdev for the suggestion
([#3]).
- Choose a 12- or 24-hour clock. A new **Time format** setting (Settings →
Appearance) lets you force a 12-hour (2:00 PM) or 24-hour (14:00) clock, or
follow the system setting automatically. It applies everywhere times appear —
the week and day timelines, agenda, event details, search and reminders — so
the format is now consistent across the whole app. Thanks to @zmaherdev for
the suggestion ([#6]).
- Optional hour lines in the timeline. A new **Hour lines** switch (Settings →
Appearance) draws a faint separator at each hour in the week and day views,
making it easier to see when events start and end. Off by default. Thanks to
@zmaherdev for the suggestion ([#5]).
- Limit how far ahead the agenda looks. New **Agenda range** and **Agenda widget
range** settings (Settings → Appearance) let the agenda screen and its widget
each show just today, the rest of this week, the rest of this month, a rolling
7 or 30 days, or a custom number of days. "This week" follows your week-start
preference. Thanks to @zmaherdev for the suggestion ([#4]).