diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/widget/WidgetEventRow.kt b/app/src/main/java/de/jeanlucmakiola/calendula/widget/WidgetEventRow.kt index 311715f..7295857 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/widget/WidgetEventRow.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/widget/WidgetEventRow.kt @@ -18,7 +18,6 @@ import androidx.glance.layout.padding import androidx.glance.text.FontWeight import androidx.glance.text.Text import androidx.glance.text.TextStyle -import de.jeanlucmakiola.calendula.MainActivity import de.jeanlucmakiola.calendula.R import de.jeanlucmakiola.calendula.domain.EventInstance import de.jeanlucmakiola.calendula.domain.isCrossedOut @@ -84,13 +83,7 @@ internal fun WidgetEventRow( .padding(horizontal = 12.dp) .clickable( actionStartActivity( - MainActivity.openEventIntent( - context = context, - eventId = event.eventId, - beginMillis = event.start.toEpochMilliseconds(), - endMillis = event.end.toEpochMilliseconds(), - source = source, - ), + event.openIntent(context, source), ), ), verticalAlignment = Alignment.CenterVertically, diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/widget/WidgetParts.kt b/app/src/main/java/de/jeanlucmakiola/calendula/widget/WidgetParts.kt index 9022f75..b790edf 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/widget/WidgetParts.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/widget/WidgetParts.kt @@ -19,6 +19,8 @@ import kotlinx.datetime.toJavaDayOfWeek import kotlinx.datetime.toJavaLocalDate import kotlinx.datetime.toLocalDateTime import kotlin.time.Instant +import de.jeanlucmakiola.calendula.MainActivity +import de.jeanlucmakiola.calendula.ui.common.CalendarView /** Cards sit one tone off the root fill, and stay opaque over a wallpaper (#292). */ @Composable @@ -27,6 +29,15 @@ internal fun widgetCardColor(): ColorProvider = when (LocalWidgetBackground.curr WidgetBackground.TINTED, WidgetBackground.TRANSPARENT -> GlanceTheme.colors.widgetBackground } +/** Opens this occurrence in the app, backing out to [source]. */ +internal fun EventInstance.openIntent(context: Context, source: CalendarView) = MainActivity.openEventIntent( + context = context, + eventId = eventId, + beginMillis = start.toEpochMilliseconds(), + endMillis = end.toEpochMilliseconds(), + source = source, +) + /** An event's time on [day]: "All day", "Starts 14:00", "Ends 9:00" or "14:00 – 15:30". */ internal fun widgetTimeLabel(context: Context, event: EventInstance, day: LocalDate, is24Hour: Boolean): String = when (val label = agendaTimeLabel(event, day, systemZone())) { 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 a3a3169..5b699dd 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 @@ -71,6 +71,7 @@ import de.jeanlucmakiola.calendula.widget.CalendulaWidgetRoot import de.jeanlucmakiola.calendula.widget.WidgetAppearance import de.jeanlucmakiola.calendula.widget.widgetCardColor import de.jeanlucmakiola.calendula.widget.widgetTimeLabel +import de.jeanlucmakiola.calendula.widget.openIntent import de.jeanlucmakiola.calendula.widget.loadAgendaWidget import de.jeanlucmakiola.calendula.widget.widgetAppearance import de.jeanlucmakiola.floret.components.Position @@ -156,10 +157,10 @@ private const val PREVIEW_ROWS = 10 * user scrolling a home-screen widget past a hundred rows is not a case worth * risking a failed update for. */ -private const val MAX_AGENDA_ROWS = 100 +internal const val MAX_AGENDA_ROWS = 100 /** Flat row model: one [LazyColumn] item per event or placeholder, a day's first one carrying its date pill. */ -private sealed interface AgendaRow { +internal sealed interface AgendaRow { val date: LocalDate data class Event(override val date: LocalDate, val event: EventInstance) : AgendaRow @@ -288,7 +289,7 @@ private fun RowList(count: Int, preview: Boolean, item: @Composable (Int) -> Uni } /** A classic list entry: a day's label line, or one of its rows. */ -private sealed interface ClassicItem { +internal sealed interface ClassicItem { val date: LocalDate data class Header(override val date: LocalDate) : ClassicItem @@ -297,6 +298,17 @@ private sealed interface ClassicItem { } } +/** + * [rows] with a label before each day's first, held to [limit] items in all, + * headers included; a label the cut leaves with nothing under it is dropped. + */ +internal fun classicItems(rows: List, limit: Int = MAX_AGENDA_ROWS): List = buildList { + rows.forEachIndexed { index, row -> + if (index == 0 || row.date != rows[index - 1].date) add(ClassicItem.Header(row.date)) + add(ClassicItem.Entry(row)) + } +}.take(limit).dropLastWhile { it is ClassicItem.Header } + /** * v2.23's look (#489): a label line per day over plain rows with a colour * stripe. On a transparent root each day sits on its own backing card, so the @@ -304,14 +316,10 @@ private sealed interface ClassicItem { */ @Composable private fun ClassicRows(rows: List, data: AgendaWidgetData.Ready, metrics: AgendaMetrics, preview: Boolean) { - val items = buildList { - rows.forEachIndexed { index, row -> - if (index == 0 || row.date != rows[index - 1].date) add(ClassicItem.Header(row.date)) - add(ClassicItem.Entry(row)) - } - } + val items = classicItems(rows) val backed = LocalWidgetBackground.current == WidgetBackground.TRANSPARENT - val positions = dayPositions(items.map { it.date }) + val positions = if (backed) dayPositions(items.map { it.date }) else emptyList() + val cardColor = widgetCardColor() val item: @Composable (Int) -> Unit = { index -> val entry = items[index] val content: @Composable () -> Unit = { @@ -333,7 +341,8 @@ private fun ClassicRows(rows: List, data: AgendaWidgetData.Ready, met WidgetGroupedRow( position = positions[index], modifier = GlanceModifier.padding(top = gap), - color = GlanceTheme.colors.widgetBackground, + color = cardColor, + gapBelow = false, content = content, ) } else { @@ -390,7 +399,7 @@ private fun ClassicEventRow(row: AgendaRow.Event, data: AgendaWidgetData.Ready, modifier = GlanceModifier .fillMaxWidth() .padding(horizontal = CLASSIC_ROW_H_PAD, vertical = metrics.rowVPad) - .clickable(actionStartActivity(event.openIntent(context))), + .clickable(actionStartActivity(event.openIntent(context, CalendarView.Agenda))), verticalAlignment = Alignment.CenterVertically, ) { Box( @@ -406,7 +415,7 @@ private fun ClassicEventRow(row: AgendaRow.Event, data: AgendaWidgetData.Ready, text = event.title.ifBlank { context.getString(R.string.event_untitled) }, maxLines = 1, style = TextStyle( - color = if (dimmed) GlanceTheme.colors.onSurfaceVariant else GlanceTheme.colors.onSurface, + color = if (dimmed) GlanceTheme.colors.outline else GlanceTheme.colors.onSurface, fontSize = metrics.eventTitle, textDecoration = glanceDeclinedDecoration(event.isCrossedOut), ), @@ -414,7 +423,10 @@ private fun ClassicEventRow(row: AgendaRow.Event, data: AgendaWidgetData.Ready, Text( text = eventTimeSummary(context, event, row.date, data.is24Hour), maxLines = 1, - style = TextStyle(color = GlanceTheme.colors.onSurfaceVariant, fontSize = metrics.eventTime), + style = TextStyle( + color = if (dimmed) GlanceTheme.colors.outline else GlanceTheme.colors.onSurfaceVariant, + fontSize = metrics.eventTime, + ), ) } } @@ -423,14 +435,6 @@ private fun ClassicEventRow(row: AgendaRow.Event, data: AgendaWidgetData.Ready, /** Space above a day's label on its own backing card. */ private val CLASSIC_CARD_TOP_PAD = 8.dp -private fun EventInstance.openIntent(context: Context) = MainActivity.openEventIntent( - context = context, - eventId = eventId, - beginMillis = start.toEpochMilliseconds(), - endMillis = end.toEpochMilliseconds(), - source = CalendarView.Agenda, -) - /** Where each row sits among the rows of its day, from each row's date, for the grouped-card shapes. */ private fun dayPositions(dates: List): List = buildList { var start = 0 @@ -584,7 +588,7 @@ private fun EventRow( .fillMaxWidth() .height(metrics.rowHeight) .padding(horizontal = ROW_H_PAD) - .clickable(actionStartActivity(event.openIntent(context))), + .clickable(actionStartActivity(event.openIntent(context, CalendarView.Agenda))), verticalAlignment = Alignment.CenterVertically, ) { Text( diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/widget/config/WidgetConfigScreen.kt b/app/src/main/java/de/jeanlucmakiola/calendula/widget/config/WidgetConfigScreen.kt index e16200d..4e93c13 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/widget/config/WidgetConfigScreen.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/widget/config/WidgetConfigScreen.kt @@ -71,6 +71,8 @@ import de.jeanlucmakiola.floret.glance.WidgetBackground import kotlin.math.roundToInt import de.jeanlucmakiola.floret.components.GroupedSliderRow import de.jeanlucmakiola.calendula.widget.AgendaWidgetStyle +import java.text.NumberFormat +import androidx.compose.runtime.remember private val PreviewMaxHeight = 280.dp @@ -245,7 +247,7 @@ private fun ConfigOptions( value = opacity, onValueChange = { onAppearance(state.appearance.copy(opacity = it)) }, position = Position.Middle, - valueLabel = stringResource(R.string.widget_config_opacity_value, (opacity * 100).roundToInt()), + valueLabel = remember(opacity) { NumberFormat.getPercentInstance().format(opacity) }, valueRange = MIN_OPACITY..1f, steps = OPACITY_STEPS, ) diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/widget/countdown/CountdownWidget.kt b/app/src/main/java/de/jeanlucmakiola/calendula/widget/countdown/CountdownWidget.kt index 975155d..df620de 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/widget/countdown/CountdownWidget.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/widget/countdown/CountdownWidget.kt @@ -42,6 +42,7 @@ import de.jeanlucmakiola.calendula.R import de.jeanlucmakiola.calendula.domain.EventInstance import de.jeanlucmakiola.calendula.domain.coversWholeDays import de.jeanlucmakiola.calendula.ui.common.CalendarView +import de.jeanlucmakiola.calendula.widget.openIntent import de.jeanlucmakiola.calendula.ui.common.eventAccent import de.jeanlucmakiola.calendula.widget.CalendulaWidgetReceiver import de.jeanlucmakiola.calendula.widget.CalendulaWidgetRoot @@ -132,13 +133,7 @@ private fun EventCountdown(event: EventInstance, data: CountdownData.Ready) { val context = LocalContext.current val label = countdownLabel(event.counted(systemZone()), data.now, systemZone()) val open = actionStartActivity( - MainActivity.openEventIntent( - context = context, - eventId = event.eventId, - beginMillis = event.start.toEpochMilliseconds(), - endMillis = event.end.toEpochMilliseconds(), - source = CalendarView.Agenda, - ), + event.openIntent(context, CalendarView.Agenda), ) // Over a wallpaper the text sits on its own card, as every other widget's rows do. val card = GlanceModifier.wallpaperBacking(radius = 16.dp) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a2503ad..e4ff9c1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -518,7 +518,6 @@ Transparent Follow app Opacity - %1$d%% Style Cards Classic diff --git a/app/src/test/java/de/jeanlucmakiola/calendula/widget/WidgetConfigTest.kt b/app/src/test/java/de/jeanlucmakiola/calendula/widget/WidgetConfigTest.kt new file mode 100644 index 0000000..a9e81e4 --- /dev/null +++ b/app/src/test/java/de/jeanlucmakiola/calendula/widget/WidgetConfigTest.kt @@ -0,0 +1,33 @@ +package de.jeanlucmakiola.calendula.widget + +import androidx.datastore.preferences.core.emptyPreferences +import androidx.datastore.preferences.core.mutablePreferencesOf +import com.google.common.truth.Truth.assertThat +import de.jeanlucmakiola.calendula.data.prefs.PastEventDisplay +import de.jeanlucmakiola.calendula.ui.agenda.AgendaRange +import de.jeanlucmakiola.floret.glance.WidgetBackground +import org.junit.jupiter.api.Test + +/** Per-widget options survive a save and read back (#489). */ +class WidgetConfigTest { + + private val defaults = AgendaWidgetOptions(AgendaRange.Week, WidgetSize.SMALL, true, PastEventDisplay.SHOW) + + @Test + fun `appearance and agenda style round-trip`() { + val appearance = WidgetAppearance(WidgetBackground.SOLID, WidgetTheme.DARK, opacity = 0.6f) + val options = defaults.copy(style = AgendaWidgetStyle.CLASSIC) + val prefs = mutablePreferencesOf().apply { + putAppearance(appearance) + putAgendaOptions(options) + } + assertThat(prefs.widgetAppearance).isEqualTo(appearance) + assertThat(prefs.agendaWidgetOptions(defaults)).isEqualTo(options) + } + + @Test + fun `a widget saved before 2_24_1 stays opaque cards`() { + assertThat(emptyPreferences().widgetAppearance.opacity).isEqualTo(1f) + assertThat(emptyPreferences().agendaWidgetOptions(defaults).style).isEqualTo(AgendaWidgetStyle.CARDS) + } +} diff --git a/app/src/test/java/de/jeanlucmakiola/calendula/widget/agenda/ClassicItemsTest.kt b/app/src/test/java/de/jeanlucmakiola/calendula/widget/agenda/ClassicItemsTest.kt new file mode 100644 index 0000000..dd525f6 --- /dev/null +++ b/app/src/test/java/de/jeanlucmakiola/calendula/widget/agenda/ClassicItemsTest.kt @@ -0,0 +1,28 @@ +package de.jeanlucmakiola.calendula.widget.agenda + +import com.google.common.truth.Truth.assertThat +import kotlinx.datetime.LocalDate +import org.junit.jupiter.api.Test + +/** The classic agenda's day labels and row cap (#489). */ +class ClassicItemsTest { + + private val mon = LocalDate(2026, 11, 9) + private val tue = LocalDate(2026, 11, 10) + + @Test + fun `each day gets one label before its rows`() { + val rows = listOf(AgendaRow.Placeholder(mon), AgendaRow.Placeholder(tue), AgendaRow.Placeholder(tue)) + assertThat(classicItems(rows).map { it::class }).containsExactly( + ClassicItem.Header::class, ClassicItem.Entry::class, + ClassicItem.Header::class, ClassicItem.Entry::class, ClassicItem.Entry::class, + ).inOrder() + } + + @Test + fun `labels count towards the cap and a stranded one is dropped`() { + val rows = listOf(AgendaRow.Placeholder(mon), AgendaRow.Placeholder(tue)) + val items = classicItems(rows, limit = 3) + assertThat(items).containsExactly(ClassicItem.Header(mon), ClassicItem.Entry(rows[0])).inOrder() + } +} diff --git a/floret-kit b/floret-kit index e62730d..b7af14a 160000 --- a/floret-kit +++ b/floret-kit @@ -1 +1 @@ -Subproject commit e62730dd18e0a3e3fd50cd327ad11fc9a80d421f +Subproject commit b7af14a31aa32ae32f3f2be2cb7e56b15ec598bf