From 4122f86ba12756f351cc70e6c46919fff2b793b0 Mon Sep 17 00:00:00 2001 From: ApolloF Date: Wed, 21 Oct 2026 23:54:16 +0200 Subject: [PATCH] Widget: Material You colours, launcher card and responsive layouts - Follows the wallpaper (Material You) colours on Android 12+, like the launcher's own widgets; older Android keeps the app's colour scheme. - Card uses android:id/background and the system widget corner radius, with the dynamic widget background instead of the near-black surface. - Status marks: taken = filled accent with a check, due = accent ring, missed = error ring, skipped = muted ring; the text says the same. The ring drawable gets a transparent fill (a tinted stroke-only shape was drawn as a solid grey circle). - Taken and skipped doses stay in the list in place, so a row keeps its position after the one-tap check. - Responsive: one row (next dose), short card (header + next dose), list from 180x150 dp. minResizeHeight 40 dp. - Picker preview: previewLayout (Android 12+) and previewImage. - Row order lives in core/domain (WidgetLines) with a test. --- .../protocoltracker/widget/TodayWidget.kt | 214 +++++++++++++----- .../drawable-nodpi/widget_preview_today.png | Bin 0 -> 37428 bytes .../main/res/drawable/widget_background.xml | 6 + app/src/main/res/drawable/widget_check.xml | 10 + .../main/res/drawable/widget_check_dot.xml | 6 + .../main/res/drawable/widget_check_ring.xml | 5 +- app/src/main/res/layout/widget_loading.xml | 5 +- .../main/res/layout/widget_preview_today.xml | 163 +++++++++++++ app/src/main/res/values-night-v31/widget.xml | 8 + app/src/main/res/values-night/widget.xml | 9 + app/src/main/res/values-v31/widget.xml | 10 + app/src/main/res/values/strings.xml | 8 + app/src/main/res/values/widget.xml | 11 + app/src/main/res/xml/today_widget_info.xml | 5 +- .../domain/schedule/WidgetLines.kt | 20 ++ .../domain/schedule/WidgetLinesTest.kt | 61 +++++ 16 files changed, 482 insertions(+), 59 deletions(-) create mode 100644 app/src/main/res/drawable-nodpi/widget_preview_today.png create mode 100644 app/src/main/res/drawable/widget_background.xml create mode 100644 app/src/main/res/drawable/widget_check.xml create mode 100644 app/src/main/res/drawable/widget_check_dot.xml create mode 100644 app/src/main/res/layout/widget_preview_today.xml create mode 100644 app/src/main/res/values-night-v31/widget.xml create mode 100644 app/src/main/res/values-night/widget.xml create mode 100644 app/src/main/res/values-v31/widget.xml create mode 100644 app/src/main/res/values/widget.xml create mode 100644 core/domain/src/main/kotlin/com/apollof/protocoltracker/domain/schedule/WidgetLines.kt create mode 100644 core/domain/src/test/kotlin/com/apollof/protocoltracker/domain/schedule/WidgetLinesTest.kt diff --git a/app/src/main/kotlin/com/apollof/protocoltracker/widget/TodayWidget.kt b/app/src/main/kotlin/com/apollof/protocoltracker/widget/TodayWidget.kt index 6879a87..9846f1b 100644 --- a/app/src/main/kotlin/com/apollof/protocoltracker/widget/TodayWidget.kt +++ b/app/src/main/kotlin/com/apollof/protocoltracker/widget/TodayWidget.kt @@ -1,7 +1,9 @@ package com.apollof.protocoltracker.widget import android.content.Context +import android.os.Build import androidx.compose.runtime.Composable +import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.glance.ColorFilter @@ -10,14 +12,17 @@ import androidx.glance.GlanceModifier import androidx.glance.GlanceTheme import androidx.glance.Image import androidx.glance.ImageProvider +import androidx.glance.LocalSize import androidx.glance.action.ActionParameters import androidx.glance.action.actionParametersOf import androidx.glance.action.actionStartActivity import androidx.glance.action.clickable import androidx.glance.appwidget.GlanceAppWidget import androidx.glance.appwidget.GlanceAppWidgetReceiver +import androidx.glance.appwidget.SizeMode import androidx.glance.appwidget.action.ActionCallback import androidx.glance.appwidget.action.actionRunCallback +import androidx.glance.appwidget.appWidgetBackground import androidx.glance.appwidget.cornerRadius import androidx.glance.appwidget.lazy.LazyColumn import androidx.glance.appwidget.lazy.items @@ -41,103 +46,206 @@ import androidx.glance.semantics.semantics import androidx.glance.text.FontWeight import androidx.glance.text.Text import androidx.glance.text.TextStyle +import androidx.glance.unit.ColorProvider import com.apollof.protocoltracker.MainActivity import com.apollof.protocoltracker.R import com.apollof.protocoltracker.container -import com.apollof.protocoltracker.data.Palette import com.apollof.protocoltracker.domain.schedule.AgendaStatus -import com.apollof.protocoltracker.domain.schedule.AgendaWindows +import com.apollof.protocoltracker.domain.schedule.WidgetLine import com.apollof.protocoltracker.domain.schedule.agendaLogsFrom import com.apollof.protocoltracker.domain.schedule.buildAgenda +import com.apollof.protocoltracker.domain.schedule.nextDose +import com.apollof.protocoltracker.domain.schedule.widgetLines import com.apollof.protocoltracker.domain.units.describeDose import com.apollof.protocoltracker.ui.theme.materialScheme import com.apollof.protocoltracker.ui.theme.trackerColors -private data class WidgetRow(val key: String, val name: String, val detail: String, val overdue: Boolean) +private enum class Mark { DUE, MISSED, TAKEN, SKIPPED } + +private data class WidgetRow(val key: String, val name: String, val detail: String, val mark: Mark) { + val checkable: Boolean get() = mark == Mark.DUE || mark == Mark.MISSED +} + +private data class WidgetState(val rows: List, val next: WidgetRow?, val done: Int, val total: Int) class TodayWidget : GlanceAppWidget() { + // One row (2x1 and wider single rows), the next dose under a header (2x2), the day's list (3x2 and up). + override val sizeMode = SizeMode.Responsive(setOf(ROW, CARD, LIST)) + override suspend fun provideGlance(context: Context, id: GlanceId) { val c = context.container val now = c.clock() val protocol = c.repository.protocolNow() val zone = c.zone() - val slotTimes = c.settings.current().slotTimes - val logs = c.repository.logsSinceNow(agendaLogsFrom(now, zone, slotTimes)) - val agenda = buildAgenda(protocol.phases, protocol.items, logs, now, zone, c.repository.anchorsNow(), slotTimes) - fun row(e: com.apollof.protocoltracker.domain.schedule.AgendaEntry, label: String): WidgetRow? { - val occ = e.occurrence ?: return null + val prefs = c.settings.current() + val logs = c.repository.logsSinceNow(agendaLogsFrom(now, zone, prefs.slotTimes)) + val agenda = buildAgenda(protocol.phases, protocol.items, logs, now, zone, c.repository.anchorsNow(), prefs.slotTimes) + val lines = agenda.widgetLines { it.dayOfWeek.getDisplayName(java.time.format.TextStyle.SHORT, java.util.Locale.getDefault()) } + fun row(line: WidgetLine): WidgetRow? { + val occ = line.entry.occurrence ?: return null val compound = protocol.compounds[occ.item.compoundId] ?: return null - return WidgetRow( - key = occ.key, name = compound.displayName, - detail = "$label · ${describeDose(occ.dose, compound.baseUnit, occ.item.formulation)}", - overdue = e.status == AgendaStatus.MISSED, - ) + val mark = when (line.entry.status) { + AgendaStatus.MISSED -> Mark.MISSED + AgendaStatus.PENDING -> Mark.DUE + AgendaStatus.TAKEN -> Mark.TAKEN + AgendaStatus.SKIPPED -> Mark.SKIPPED + } + val status = when (mark) { + Mark.MISSED -> "Missed · " + Mark.TAKEN -> "Taken · " + Mark.SKIPPED -> "Skipped · " + Mark.DUE -> "" + } + val dose = describeDose(occ.dose, compound.baseUnit, occ.item.formulation) + return WidgetRow(occ.key, compound.displayName, "$status${line.label} · $dose", mark) } - val rows = agenda.missed.mapNotNull { row(it, it.occurrence!!.localDate.dayOfWeek.getDisplayName(java.time.format.TextStyle.SHORT, java.util.Locale.getDefault())) } + - agenda.groups.flatMap { g -> g.pending.mapNotNull { row(it, g.label) } } - val done = agenda.doneToday - // The app's scheme, following the system light/dark mode; the wallpaper scheme uses Glance's dynamic colours. - val prefs = c.settings.current() - val colors = if (prefs.palette == Palette.DYNAMIC) null else ColorProviders( + val state = WidgetState(lines.mapNotNull(::row), lines.nextDose()?.let(::row), agenda.doneToday, agenda.scheduledToday) + // Wallpaper colours on Android 12+, like the launcher's own widgets; before that the app's colour scheme. + val colors = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) null else ColorProviders( light = materialScheme(trackerColors(prefs.palette, dark = false)), dark = materialScheme(trackerColors(prefs.palette, dark = true, pureBlack = prefs.pureBlack)), ) provideContent { - if (colors == null) GlanceTheme { Content(rows, done) } else GlanceTheme(colors = colors) { Content(rows, done) } + if (colors == null) GlanceTheme { Content(state) } else GlanceTheme(colors = colors) { Content(state) } } } @Composable - private fun Content(rows: List, done: Int) { - Column( - modifier = GlanceModifier.fillMaxSize().background(GlanceTheme.colors.background).cornerRadius(20.dp).padding(12.dp), - ) { - Row( - modifier = GlanceModifier.fillMaxWidth().clickable(actionStartActivity()), - verticalAlignment = Alignment.CenterVertically, - ) { - Text("Today", style = TextStyle(fontWeight = FontWeight.Bold, fontSize = 16.sp, color = GlanceTheme.colors.onSurface)) - Spacer(GlanceModifier.defaultWeight()) - Text("$done done", style = TextStyle(fontSize = 12.sp, color = GlanceTheme.colors.onSurfaceVariant)) + private fun Content(state: WidgetState) { + val size = LocalSize.current + Box(modifier = GlanceModifier.fillMaxSize().widgetCard()) { + when { + size.width >= LIST.width && size.height >= LIST.height -> ListLayout(state) + size.height >= CARD.height -> CardLayout(state) + else -> RowLayout(state) } - Spacer(GlanceModifier.height(8.dp)) - if (rows.isEmpty()) { - Text("Nothing left today", style = TextStyle(fontSize = 14.sp, color = GlanceTheme.colors.onSurfaceVariant)) + } + } + + @Composable + private fun ListLayout(state: WidgetState) { + Column(modifier = GlanceModifier.fillMaxSize().padding(start = 8.dp, top = 12.dp, end = 12.dp)) { + Header(state) + Spacer(GlanceModifier.height(4.dp)) + if (state.rows.isEmpty()) { + Text( + "Nothing left today", modifier = GlanceModifier.padding(horizontal = 8.dp), + style = TextStyle(fontSize = 14.sp, color = GlanceTheme.colors.onSurfaceVariant), + ) } else { - LazyColumn { - items(rows, itemId = { it.key.hashCode().toLong() }) { row -> RowItem(row) } + LazyColumn(modifier = GlanceModifier.fillMaxSize()) { + items(state.rows, itemId = { it.key.hashCode().toLong() }) { row -> DoseRow(row) } } } } } @Composable - private fun RowItem(row: WidgetRow) { - Row(modifier = GlanceModifier.fillMaxWidth().padding(vertical = 4.dp), verticalAlignment = Alignment.CenterVertically) { - // An empty ring, like the in-app check: pending doses never look taken. - Box( - modifier = GlanceModifier.size(48.dp) - .clickable(actionRunCallback(actionParametersOf(CheckAction.KEY to row.key))) - .semantics { contentDescription = "Mark ${row.name} taken" }, - contentAlignment = Alignment.Center, - ) { - Image( - ImageProvider(R.drawable.widget_check_ring), contentDescription = null, - colorFilter = ColorFilter.tint(GlanceTheme.colors.outline), modifier = GlanceModifier.size(36.dp), - ) - } - Spacer(GlanceModifier.width(10.dp)) + private fun CardLayout(state: WidgetState) { + Column(modifier = GlanceModifier.fillMaxSize().padding(start = 8.dp, top = 12.dp, end = 12.dp, bottom = 8.dp)) { + Header(state) + Spacer(GlanceModifier.defaultWeight()) + NextOrDone(state) + } + } + + @Composable + private fun RowLayout(state: WidgetState) { + Row( + modifier = GlanceModifier.fillMaxSize().padding(start = 4.dp, end = 16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Box(modifier = GlanceModifier.defaultWeight()) { NextOrDone(state) } + Text( + "${state.done}/${state.total}", modifier = GlanceModifier.clickable(actionStartActivity()), + style = TextStyle(fontWeight = FontWeight.Bold, fontSize = 14.sp, color = GlanceTheme.colors.primary), + ) + } + } + + @Composable + private fun NextOrDone(state: WidgetState) { + val next = state.next + if (next != null) { + DoseRow(next) + } else { + val detail = if (state.total == 0) "No doses planned" else "${state.done} of ${state.total} done" + DoseRow(WidgetRow("", "Nothing left today", detail, Mark.TAKEN)) + } + } + + @Composable + private fun Header(state: WidgetState) { + Row( + modifier = GlanceModifier.fillMaxWidth().padding(horizontal = 8.dp).clickable(actionStartActivity()), + verticalAlignment = Alignment.CenterVertically, + ) { + Text("Today", style = TextStyle(fontWeight = FontWeight.Bold, fontSize = 16.sp, color = GlanceTheme.colors.onSurface)) + Spacer(GlanceModifier.defaultWeight()) + Text( + "${state.done} of ${state.total} done", maxLines = 1, + style = TextStyle(fontWeight = FontWeight.Medium, fontSize = 13.sp, color = GlanceTheme.colors.primary), + ) + } + } + + @Composable + private fun DoseRow(row: WidgetRow) { + Row(modifier = GlanceModifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { + StatusMark(row) + Spacer(GlanceModifier.width(4.dp)) Column(modifier = GlanceModifier.defaultWeight().clickable(actionStartActivity())) { Text(row.name, maxLines = 1, style = TextStyle(fontWeight = FontWeight.Medium, fontSize = 14.sp, color = GlanceTheme.colors.onSurface)) Text( - (if (row.overdue) "Missed · " else "") + row.detail, maxLines = 1, - style = TextStyle(fontSize = 12.sp, color = if (row.overdue) GlanceTheme.colors.error else GlanceTheme.colors.onSurfaceVariant), + row.detail, maxLines = 1, + style = TextStyle(fontSize = 12.sp, color = if (row.mark == Mark.MISSED) GlanceTheme.colors.error else GlanceTheme.colors.onSurfaceVariant), ) } } } + /** Taken: filled accent with a check. Due: accent ring. Missed: error ring. Skipped: muted ring. The text says the same. */ + @Composable + private fun StatusMark(row: WidgetRow) { + val target = if (row.checkable) { + GlanceModifier.clickable(actionRunCallback(actionParametersOf(CheckAction.KEY to row.key))) + .semantics { contentDescription = "Mark ${row.name} taken" } + } else { + GlanceModifier + } + Box(modifier = GlanceModifier.size(48.dp).then(target), contentAlignment = Alignment.Center) { + if (row.mark == Mark.TAKEN) { + Glyph(R.drawable.widget_check_dot, GlanceTheme.colors.primary, 28) + Glyph(R.drawable.widget_check, GlanceTheme.colors.onPrimary, 18) + } else { + val tint = when (row.mark) { + Mark.MISSED -> GlanceTheme.colors.error + Mark.SKIPPED -> GlanceTheme.colors.outline + else -> GlanceTheme.colors.primary + } + Glyph(R.drawable.widget_check_ring, tint, 28) + } + } + } + + @Composable + private fun Glyph(drawable: Int, tint: ColorProvider, sizeDp: Int) { + Image(ImageProvider(drawable), contentDescription = null, colorFilter = ColorFilter.tint(tint), modifier = GlanceModifier.size(sizeDp.dp)) + } + + /** The launcher's widget card: system corner radius and android:id/background, so launchers can restyle it. */ + @Composable + private fun GlanceModifier.widgetCard(): GlanceModifier = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + appWidgetBackground().background(GlanceTheme.colors.widgetBackground).cornerRadius(android.R.dimen.system_app_widget_background_radius) + } else { + appWidgetBackground().background(ImageProvider(R.drawable.widget_background), colorFilter = ColorFilter.tint(GlanceTheme.colors.surfaceVariant)) + } + companion object { + private val ROW = DpSize(110.dp, 40.dp) + private val CARD = DpSize(110.dp, 100.dp) + private val LIST = DpSize(180.dp, 150.dp) + suspend fun refresh(context: Context) = TodayWidget().updateAll(context) } } diff --git a/app/src/main/res/drawable-nodpi/widget_preview_today.png b/app/src/main/res/drawable-nodpi/widget_preview_today.png new file mode 100644 index 0000000000000000000000000000000000000000..4d996559ea83fc50cdfe7aeba36c3dbcb18a30d0 GIT binary patch literal 37428 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 Aq5uE@ literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/widget_background.xml b/app/src/main/res/drawable/widget_background.xml new file mode 100644 index 0000000..96ab816 --- /dev/null +++ b/app/src/main/res/drawable/widget_background.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/app/src/main/res/drawable/widget_check.xml b/app/src/main/res/drawable/widget_check.xml new file mode 100644 index 0000000..2301219 --- /dev/null +++ b/app/src/main/res/drawable/widget_check.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/drawable/widget_check_dot.xml b/app/src/main/res/drawable/widget_check_dot.xml new file mode 100644 index 0000000..3cff6fd --- /dev/null +++ b/app/src/main/res/drawable/widget_check_dot.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/app/src/main/res/drawable/widget_check_ring.xml b/app/src/main/res/drawable/widget_check_ring.xml index f691cc6..6cf4f9f 100644 --- a/app/src/main/res/drawable/widget_check_ring.xml +++ b/app/src/main/res/drawable/widget_check_ring.xml @@ -1,6 +1,7 @@ - + + - + diff --git a/app/src/main/res/layout/widget_loading.xml b/app/src/main/res/layout/widget_loading.xml index 7943b4f..dc6f349 100644 --- a/app/src/main/res/layout/widget_loading.xml +++ b/app/src/main/res/layout/widget_loading.xml @@ -1,7 +1,6 @@ + android:background="@drawable/widget_background" /> diff --git a/app/src/main/res/layout/widget_preview_today.xml b/app/src/main/res/layout/widget_preview_today.xml new file mode 100644 index 0000000..ca82d6a --- /dev/null +++ b/app/src/main/res/layout/widget_preview_today.xml @@ -0,0 +1,163 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values-night-v31/widget.xml b/app/src/main/res/values-night-v31/widget.xml new file mode 100644 index 0000000..9b56ec8 --- /dev/null +++ b/app/src/main/res/values-night-v31/widget.xml @@ -0,0 +1,8 @@ + + + @android:color/system_accent2_800 + @android:color/system_accent1_200 + @android:color/system_accent1_800 + @android:color/system_neutral1_100 + @android:color/system_neutral2_200 + diff --git a/app/src/main/res/values-night/widget.xml b/app/src/main/res/values-night/widget.xml new file mode 100644 index 0000000..1cb2f90 --- /dev/null +++ b/app/src/main/res/values-night/widget.xml @@ -0,0 +1,9 @@ + + + #FF222826 + #FF7CCBB5 + #FF06251E + #FFE7ECE9 + #FF9AA59F + #FFF2B8B5 + diff --git a/app/src/main/res/values-v31/widget.xml b/app/src/main/res/values-v31/widget.xml new file mode 100644 index 0000000..9de2482 --- /dev/null +++ b/app/src/main/res/values-v31/widget.xml @@ -0,0 +1,10 @@ + + + + @android:color/system_accent2_50 + @android:color/system_accent1_600 + @android:color/system_accent1_0 + @android:color/system_neutral1_900 + @android:color/system_neutral2_700 + @android:dimen/system_app_widget_background_radius + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c8b61bc..8e1664b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -3,6 +3,14 @@ SteroidTracker Today Today\'s doses with one-tap check + + 1 of 3 + Vitamin D3 + Taken · Morning · 2000 IU + Omega-3 + Morning · 1 g + Magnesium + Evening · 400 mg Doses Reminder at each scheduled dose time Daily summary diff --git a/app/src/main/res/values/widget.xml b/app/src/main/res/values/widget.xml new file mode 100644 index 0000000..7721595 --- /dev/null +++ b/app/src/main/res/values/widget.xml @@ -0,0 +1,11 @@ + + + + #FFE8ECE9 + #FF1E6B5C + #FFFFFFFF + #FF151917 + #FF56605B + #FFB3261E + 20dp + diff --git a/app/src/main/res/xml/today_widget_info.xml b/app/src/main/res/xml/today_widget_info.xml index 9a84c9b..b58dcc1 100644 --- a/app/src/main/res/xml/today_widget_info.xml +++ b/app/src/main/res/xml/today_widget_info.xml @@ -1,12 +1,15 @@ + String): List = + missed.mapNotNull { e -> e.occurrence?.let { WidgetLine(e, dayLabel(it.localDate)) } } + + groups.flatMap { g -> g.entries.filter { it.occurrence != null }.map { WidgetLine(it, g.label) } } + +/** The dose the compact widget shows: the first one still to log, missed doses first. */ +fun List.nextDose(): WidgetLine? = firstOrNull { it.checkable } diff --git a/core/domain/src/test/kotlin/com/apollof/protocoltracker/domain/schedule/WidgetLinesTest.kt b/core/domain/src/test/kotlin/com/apollof/protocoltracker/domain/schedule/WidgetLinesTest.kt new file mode 100644 index 0000000..b606926 --- /dev/null +++ b/core/domain/src/test/kotlin/com/apollof/protocoltracker/domain/schedule/WidgetLinesTest.kt @@ -0,0 +1,61 @@ +package com.apollof.protocoltracker.domain.schedule + +import com.apollof.protocoltracker.domain.model.Amount +import com.apollof.protocoltracker.domain.model.BaseUnit +import com.apollof.protocoltracker.domain.model.CompoundCategory +import com.apollof.protocoltracker.domain.model.DaySlot +import com.apollof.protocoltracker.domain.model.DoseLog +import com.apollof.protocoltracker.domain.model.DoseSnapshot +import com.apollof.protocoltracker.domain.model.DoseUnit +import com.apollof.protocoltracker.domain.model.LogStatus +import com.apollof.protocoltracker.domain.model.PlanItem +import com.apollof.protocoltracker.domain.model.Schedule +import com.apollof.protocoltracker.domain.model.Timing +import java.time.Instant +import java.time.LocalDate +import java.time.LocalTime +import java.time.ZoneId +import java.time.ZonedDateTime +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull + +class WidgetLinesTest { + private val zone = ZoneId.of("Europe/Amsterdam") + private val morning = PlanItem("m", null, "c", Amount(40.0, DoseUnit.MG), schedule = Schedule.Daily(listOf(Timing.Slot(DaySlot.MORNING))), sortOrder = 1) + private val pre = PlanItem("p", null, "c", Amount(50.0, DoseUnit.MG), schedule = Schedule.Daily(listOf(Timing.Slot(DaySlot.PRE_WORKOUT))), sortOrder = 2) + + private fun at(date: String, time: String) = ZonedDateTime.of(LocalDate.parse(date), LocalTime.parse(time), zone).toInstant() + private fun log(key: String, takenAt: Instant, status: LogStatus = LogStatus.TAKEN) = DoseLog( + key, "i", "c", key, null, takenAt, Amount(1.0, DoseUnit.MG), null, status, + snapshot = DoseSnapshot("C", "C", CompoundCategory.SUPPORT, BaseUnit.MG, null), createdAt = takenAt, + ) + + @Test + fun missedFirstThenTodayInPlanOrderWithDoneInPlace() { + val now = at("2026-09-25", "09:00") + val logs = listOf( + log(slotOccurrenceKey("m", LocalDate.parse("2026-09-25"), DaySlot.MORNING), at("2026-09-25", "08:10")), + log(slotOccurrenceKey("m", LocalDate.parse("2026-09-24"), DaySlot.MORNING), at("2026-09-24", "08:10")), + log(slotOccurrenceKey("m", LocalDate.parse("2026-09-23"), DaySlot.MORNING), at("2026-09-23", "08:10")), + ) + val lines = buildAgenda(emptyList(), listOf(morning, pre), logs, now, zone, IntervalAnchors.NONE) + .widgetLines { it.dayOfWeek.name.take(3) } + + assertEquals(listOf("WED", "THU", "Morning", "Pre-workout"), lines.map { it.label }) + assertEquals(listOf(true, true, false, true), lines.map { it.checkable }) + assertEquals(AgendaStatus.MISSED, lines.nextDose()!!.entry.status) + } + + @Test + fun nothingLeftHasNoNextDose() { + val now = at("2026-09-25", "09:00") + val key = slotOccurrenceKey("m", LocalDate.parse("2026-09-25"), DaySlot.MORNING) + val logs = listOf(log(key, at("2026-09-25", "08:10"), LogStatus.SKIPPED)) + val lines = buildAgenda(emptyList(), listOf(morning), logs, now, zone, IntervalAnchors.NONE).widgetLines { "" } + .filter { it.label == "Morning" } + + assertEquals(listOf(AgendaStatus.SKIPPED), lines.map { it.entry.status }) + assertNull(lines.nextDose()) + } +}