Files
honeyDueKMP/composeApp/src/androidMain/kotlin/com/tt/honeyDue/widget/HoneyDueMediumWidget.kt
Trey T 1fcb456ef1 P3 Stream K: Glance widgets (small/medium/large) matching iOS
- HoneyDueSmallWidget (2x2), HoneyDueMediumWidget (4x2),
  HoneyDueLargeWidget (4x4) rewritten to consume the
  iOS-parity WidgetDataRepository (Stream J).
- Free-tier shows a large count-only layout (matches iOS
  FreeWidgetView); premium shows task list + complete buttons
  (Large widget also renders the Overdue / 7 Days / 30 Days
  stats row from WidgetDataRepository.computeStats()).
- WidgetFormatter mirrors iOS formatWidgetDate: "Today" /
  "in N day(s)" / "N day(s) ago".
- WidgetColors maps priority levels (1-4) to primary/yellow/
  accent/error, matching iOS OrganicTaskRowView.priorityColor.
- WidgetUi shared Glance composables (TaskRow, WidgetHeader,
  EmptyState, TaskCountBlock, StatPill, StatsRow, CompleteButton)
  wired to Stream M's CompleteTaskAction for interactive rows.
- JVM tests: WidgetFormatterTest + WidgetColorsTest covering
  10 formatter assertions and 11 color mapping assertions.

Glance caveats: no radial/linear gradients or custom shapes, so
iOS's "organic" glows are dropped in favor of cream background +
tinted TaskRow cards. Colors, typography, and priority semantics
match iOS exactly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 12:55:08 -05:00

126 lines
4.5 KiB
Kotlin

package com.tt.honeyDue.widget
import android.content.Context
import androidx.compose.runtime.Composable
import androidx.compose.ui.unit.dp
import androidx.glance.GlanceId
import androidx.glance.GlanceModifier
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.actionRunCallback
import androidx.glance.appwidget.provideContent
import androidx.glance.background
import androidx.glance.layout.Alignment
import androidx.glance.layout.Box
import androidx.glance.layout.Column
import androidx.glance.layout.Row
import androidx.glance.layout.Spacer
import androidx.glance.layout.fillMaxHeight
import androidx.glance.layout.fillMaxSize
import androidx.glance.layout.height
import androidx.glance.layout.padding
import androidx.glance.layout.width
/**
* Medium (4x2) widget.
*
* Mirrors iOS `MediumWidgetView`: left-side big task count + vertical
* divider + right-side list of the next 2-3 tasks. Free tier collapses
* to the count-only layout.
*/
class HoneyDueMediumWidget : GlanceAppWidget() {
override val sizeMode: SizeMode = SizeMode.Single
override suspend fun provideGlance(context: Context, id: GlanceId) {
val repo = WidgetDataRepository.get(context)
val tasks = repo.loadTasks()
val tier = repo.loadTierState()
val isPremium = tier.equals("premium", ignoreCase = true)
provideContent {
MediumWidgetContent(tasks, isPremium)
}
}
@Composable
private fun MediumWidgetContent(
tasks: List<WidgetTaskDto>,
isPremium: Boolean
) {
val openApp = actionRunCallback<OpenAppAction>()
Box(
modifier = GlanceModifier
.fillMaxSize()
.background(WidgetColors.BACKGROUND_PRIMARY)
.padding(12.dp)
.clickable(openApp)
) {
if (!isPremium) {
Column(
modifier = GlanceModifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalAlignment = Alignment.CenterVertically
) {
TaskCountBlock(count = tasks.size, long = true)
}
} else {
Row(
modifier = GlanceModifier.fillMaxSize(),
verticalAlignment = Alignment.CenterVertically
) {
// Left: big count
Box(
modifier = GlanceModifier.width(90.dp).fillMaxHeight(),
contentAlignment = Alignment.Center
) {
TaskCountBlock(count = tasks.size, long = false)
}
// Thin divider
Box(
modifier = GlanceModifier
.width(1.dp)
.fillMaxHeight()
.padding(vertical = 12.dp)
.background(WidgetColors.TEXT_SECONDARY)
) {}
Spacer(modifier = GlanceModifier.width(10.dp))
// Right: task list (max 3) or empty state
Column(
modifier = GlanceModifier.defaultWeight().fillMaxHeight()
) {
if (tasks.isEmpty()) {
EmptyState(compact = true, onTap = openApp)
} else {
val shown = tasks.take(3)
shown.forEachIndexed { index, task ->
TaskRow(
task = task,
compact = true,
showResidence = false,
onTaskClick = openApp,
trailing = { CompleteButton(taskId = task.id, compact = true) }
)
if (index < shown.lastIndex) {
Spacer(modifier = GlanceModifier.height(4.dp))
}
}
}
}
}
}
}
}
}
/** AppWidget receiver for the medium widget. */
class HoneyDueMediumWidgetReceiver : GlanceAppWidgetReceiver() {
override val glanceAppWidget: GlanceAppWidget = HoneyDueMediumWidget()
}