Classic agenda: cap with labels, visible dimming, solid day cards (#489)
This commit is contained in:
1 parent
fffb033e39
commit
aacd86cebe
9 files changed
+106
-41
No files matched your search
@@ -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,
|
||||
|
||||
@@ -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())) {
|
||||
|
||||
@@ -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<AgendaRow>, limit: Int = MAX_AGENDA_ROWS): List<ClassicItem> = 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<AgendaRow>, 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<AgendaRow>, 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<LocalDate>): List<Position> = 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(
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -518,7 +518,6 @@
|
||||
<string name="widget_config_background_transparent">Transparent</string>
|
||||
<string name="widget_config_theme_follow_app">Follow app</string>
|
||||
<string name="widget_config_opacity">Opacity</string>
|
||||
<string name="widget_config_opacity_value">%1$d%%</string>
|
||||
<string name="widget_config_style">Style</string>
|
||||
<string name="widget_config_style_cards">Cards</string>
|
||||
<string name="widget_config_style_classic">Classic</string>
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
+1
-1
Submodule floret-kit updated: e62730dd18...b7af14a31a.
Reference in new issue
Block a user