diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/agenda/AgendaRange.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/agenda/AgendaRange.kt index 1ae2046..b4cda3f 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/agenda/AgendaRange.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/agenda/AgendaRange.kt @@ -1,10 +1,9 @@ package de.jeanlucmakiola.calendula.ui.agenda +import de.jeanlucmakiola.calendula.ui.common.localizedDateFormatter import kotlinx.datetime.DayOfWeek import kotlinx.datetime.LocalDate import java.time.YearMonth -import java.time.format.DateTimeFormatter -import java.time.format.FormatStyle import java.util.Locale /** @@ -87,10 +86,12 @@ fun parseAgendaRange(stored: String?, default: AgendaRange): AgendaRange = when /** * The concrete span the range covers, starting at [start] through [end] - * (inclusive), for a human-readable header: - * - [AgendaRange.Day] → a single medium date ("27 Jun 2026") + * (inclusive), for a human-readable header. Both ends use the same day-month + * order so a span never mixes "15 Jul" with "Aug 13, 2026": + * - [AgendaRange.Day] → a single date ("27 Jun 2026") * - [AgendaRange.ThisMonth] → month and year ("June 2026") - * - everything else → "start – end" ("27 Jun – 3 Jul 2026") + * - everything else → "start – end" ("27 Jun – 3 Jul 2026"), with the start's + * year shown too only when it differs from the end's. */ fun agendaRangeWindowSummary( range: AgendaRange, @@ -100,11 +101,15 @@ fun agendaRangeWindowSummary( ): String { val javaStart = java.time.LocalDate.of(start.year, start.month.ordinal + 1, start.day) val javaEnd = java.time.LocalDate.of(end.year, end.month.ordinal + 1, end.day) - val medium = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM).withLocale(locale) + val dayMonth = localizedDateFormatter(locale, "dMMM") + val dayMonthYear = localizedDateFormatter(locale, "dMMMy") return when (range) { - AgendaRange.Day -> medium.format(javaStart) - AgendaRange.ThisMonth -> DateTimeFormatter.ofPattern("LLLL yyyy", locale).format(javaStart) - else -> "${DateTimeFormatter.ofPattern("d MMM", locale).format(javaStart)} – ${medium.format(javaEnd)}" + AgendaRange.Day -> dayMonthYear.format(javaStart) + AgendaRange.ThisMonth -> localizedDateFormatter(locale, "LLLLy").format(javaStart) + else -> { + val startFmt = if (start.year == end.year) dayMonth else dayMonthYear + "${startFmt.format(javaStart)} – ${dayMonthYear.format(javaEnd)}" + } } } diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/agenda/AgendaScreen.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/agenda/AgendaScreen.kt index 6d7785c..5671d5a 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/agenda/AgendaScreen.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/agenda/AgendaScreen.kt @@ -15,17 +15,17 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Coffee -import androidx.compose.material.icons.filled.DateRange import androidx.compose.material.icons.filled.Menu import androidx.compose.material.icons.filled.Search +import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Card import androidx.compose.material3.DrawerValue +import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -66,6 +66,7 @@ import de.jeanlucmakiola.calendula.ui.common.CalendarFabColumn import de.jeanlucmakiola.calendula.ui.common.CalendarFailure import de.jeanlucmakiola.calendula.ui.common.CalendarView import de.jeanlucmakiola.calendula.ui.common.IMPLEMENTED_VIEWS +import de.jeanlucmakiola.calendula.ui.common.localizedDateFormatter import de.jeanlucmakiola.calendula.ui.common.EventDimAlpha import de.jeanlucmakiola.floret.components.GroupedRow import de.jeanlucmakiola.floret.components.Position @@ -84,7 +85,6 @@ import kotlinx.datetime.TimeZone import kotlinx.datetime.plus import kotlinx.datetime.toLocalDateTime import kotlin.time.Instant -import java.time.format.TextStyle as JavaTextStyle import java.util.Locale private val zone = TimeZone.currentSystemDefault() @@ -172,9 +172,11 @@ fun AgendaScreen( successState?.takeIf { it.showRangeBar }?.let { s -> Row( verticalAlignment = Alignment.CenterVertically, + // end aligns the selector's right edge with the top-bar view + // switcher (its 8.dp margin + the app bar's 4.dp inset). modifier = Modifier .fillMaxWidth() - .padding(start = 28.dp, end = 16.dp, top = 8.dp, bottom = 8.dp), + .padding(start = 28.dp, end = 12.dp, top = 8.dp, bottom = 8.dp), ) { AgendaRangeBanner( range = s.range, @@ -216,9 +218,11 @@ fun AgendaScreen( } /** - * A compact tonal pill showing the agenda's current range. Tapping it opens the - * range picker as a session-only override. Filled with the primary container - * while an override is active, so the temporary state is obvious. + * The agenda's current range, tapped to open the range picker as a session-only + * override. Shares the top-bar view switcher's button shape so the two read as a + * family, but stays low-emphasis — a subtle neutral surface tint rather than the + * switcher's secondary container, so it doesn't compete. Fills with the primary + * container only while an override is active, to make that temporary state clear. */ @Composable private fun AgendaRangePill( @@ -227,43 +231,34 @@ private fun AgendaRangePill( onClick: () -> Unit, modifier: Modifier = Modifier, ) { - val container = if (isOverride) { - MaterialTheme.colorScheme.primaryContainer - } else { - MaterialTheme.colorScheme.surfaceContainerHigh - } - val content = if (isOverride) { - MaterialTheme.colorScheme.onPrimaryContainer - } else { - MaterialTheme.colorScheme.onSurfaceVariant - } - Surface( - color = container, - contentColor = content, - shape = RoundedCornerShape(50), - modifier = modifier.clickable(onClick = onClick), + FilledTonalButton( + onClick = onClick, + shape = MaterialTheme.shapes.large, + colors = ButtonDefaults.filledTonalButtonColors( + containerColor = if (isOverride) { + MaterialTheme.colorScheme.primaryContainer + } else { + MaterialTheme.colorScheme.surfaceContainerHigh + }, + contentColor = if (isOverride) { + MaterialTheme.colorScheme.onPrimaryContainer + } else { + MaterialTheme.colorScheme.onSurfaceVariant + }, + ), + modifier = modifier, ) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.padding(horizontal = 16.dp, vertical = 10.dp), - ) { - Icon( - imageVector = Icons.Filled.DateRange, - contentDescription = null, - modifier = Modifier.size(18.dp), - ) - Spacer(Modifier.width(8.dp)) - Text( - text = agendaRangeLabel(range), - style = MaterialTheme.typography.labelLarge, - ) - } + Text( + text = agendaRangeLabel(range), + style = MaterialTheme.typography.labelLarge, + ) } } /** - * A header naming the concrete window currently shown, e.g. "Showing all events - * for · Today, 27 Jun 2026" / "This week, 27 Jun – 3 Jul" / "This month, June 2026". + * A header naming the concrete window currently shown under a "showing …" label, + * e.g. "27 Jun 2026" / "27 Jun – 3 Jul 2026" / "June 2026". The range's name + * lives on the selector button beside it, so it isn't repeated here. */ @Composable private fun AgendaRangeBanner( @@ -280,8 +275,10 @@ private fun AgendaRangeBanner( style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, ) + // Just the concrete dates — the range's name ("Next 30 days") already + // sits on the selector button to the right, so repeating it here is noise. Text( - text = "${agendaRangeLabel(range)}, $window", + text = window, style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.onSurface, ) @@ -418,6 +415,8 @@ private fun AgendaDayHeader( @Composable private fun AgendaEmptyDayRow(onClick: () -> Unit) { Card( + // Match a single event row's resting corner radius (floret groupedShape). + shape = RoundedCornerShape(22.dp), modifier = Modifier .fillMaxWidth() .padding(horizontal = 16.dp, vertical = 4.dp) @@ -575,7 +574,7 @@ private fun formatTime(instant: Instant, is24Hour: Boolean, locale: Locale): Str private fun formatAgendaDate(date: LocalDate): String { val locale = Locale.getDefault() val java = java.time.LocalDate.of(date.year, date.month.ordinal + 1, date.day) - val weekday = java.dayOfWeek.getDisplayName(JavaTextStyle.SHORT, locale) - val monthName = java.month.getDisplayName(JavaTextStyle.SHORT, locale) - return "$weekday, ${date.day}. $monthName ${date.year}" + // Weekday + date in the locale's own field order (e.g. "Wed, Jun 17, 2026" + // vs "Mi., 17. Juni 2026") rather than a hardcoded day-month-year layout. + return localizedDateFormatter(locale, "EEEdMMMy").format(java) } diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/LocaleSupport.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/LocaleSupport.kt index c3a72a4..8fc28ef 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/LocaleSupport.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/LocaleSupport.kt @@ -4,6 +4,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalConfiguration import androidx.core.os.ConfigurationCompat +import java.time.format.DateTimeFormatter import java.util.Locale /** @@ -18,3 +19,15 @@ fun currentLocale(): Locale { ConfigurationCompat.getLocales(configuration).get(0) ?: Locale.getDefault() } } + +/** + * A [DateTimeFormatter] for [skeleton]'s fields laid out in [locale]'s own order + * (via Android's best-pattern matching), so dates read naturally per locale + * instead of a hardcoded day-month-year order. [skeleton] lists the wanted + * fields, e.g. "dMMMy" (day, abbreviated month, year) or "EEEdMMM" (weekday too). + */ +fun localizedDateFormatter(locale: Locale, skeleton: String): DateTimeFormatter = + DateTimeFormatter.ofPattern( + android.text.format.DateFormat.getBestDateTimePattern(locale, skeleton), + locale, + ) diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/widget/agenda/AgendaWidget.kt b/app/src/main/java/de/jeanlucmakiola/calendula/widget/agenda/AgendaWidget.kt index ca2ce11..b0c0604 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/widget/agenda/AgendaWidget.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/widget/agenda/AgendaWidget.kt @@ -55,6 +55,7 @@ import de.jeanlucmakiola.calendula.ui.agenda.parseAgendaRange import de.jeanlucmakiola.calendula.ui.common.CalendarView import de.jeanlucmakiola.calendula.ui.common.EventDimAlpha import de.jeanlucmakiola.calendula.ui.common.formatTimeOfDay +import de.jeanlucmakiola.calendula.ui.common.localizedDateFormatter import de.jeanlucmakiola.floret.components.pastelize import de.jeanlucmakiola.calendula.widget.AgendaWidgetData import de.jeanlucmakiola.calendula.widget.CalendulaGlanceTheme @@ -67,7 +68,6 @@ import kotlinx.datetime.TimeZone import kotlinx.datetime.plus import kotlinx.datetime.toLocalDateTime import kotlin.time.Instant -import java.time.format.TextStyle as JavaTextStyle import java.util.Locale /** @@ -399,9 +399,9 @@ private fun agendaDayLabel(context: Context, date: LocalDate, today: LocalDate): } val locale = Locale.getDefault() val java = java.time.LocalDate.of(date.year, date.month.ordinal + 1, date.day) - val weekday = java.dayOfWeek.getDisplayName(JavaTextStyle.SHORT, locale) - val monthName = java.month.getDisplayName(JavaTextStyle.SHORT, locale) - val formatted = "$weekday, ${date.day} $monthName" + // Weekday + date in the locale's own field order (compact: no year), rather + // than a hardcoded day-month layout. + val formatted = localizedDateFormatter(locale, "EEEdMMM").format(java) return if (relative != null) "$relative · $formatted" else formatted }