1fcb456ef1
- 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>
139 lines
5.2 KiB
Kotlin
139 lines
5.2 KiB
Kotlin
package com.tt.honeyDue.widget
|
|
|
|
import android.content.Context
|
|
import androidx.compose.runtime.Composable
|
|
import androidx.compose.ui.unit.dp
|
|
import androidx.compose.ui.unit.sp
|
|
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.Spacer
|
|
import androidx.glance.layout.fillMaxSize
|
|
import androidx.glance.layout.fillMaxWidth
|
|
import androidx.glance.layout.height
|
|
import androidx.glance.layout.padding
|
|
import androidx.glance.text.FontWeight
|
|
import androidx.glance.text.Text
|
|
import androidx.glance.text.TextStyle
|
|
|
|
/**
|
|
* Large (4x4) widget.
|
|
*
|
|
* Mirrors iOS `LargeWidgetView`:
|
|
* - When there are tasks: list of up to 5 tasks with residence/due
|
|
* labels, optional "+N more" text, and a 3-pill stats row at the
|
|
* bottom (Overdue / 7 Days / 30 Days).
|
|
* - When empty: centered "All caught up!" state above the stats.
|
|
* - Free tier collapses to the count-only layout.
|
|
*
|
|
* Glance restriction: no LazyColumn here because the list is bounded
|
|
* (max 5), so a plain Column is fine and lets us compose the stats row
|
|
* at the bottom without nesting a second scroll container.
|
|
*/
|
|
class HoneyDueLargeWidget : 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 stats = repo.computeStats()
|
|
val tier = repo.loadTierState()
|
|
val isPremium = tier.equals("premium", ignoreCase = true)
|
|
|
|
provideContent {
|
|
LargeWidgetContent(tasks, stats, isPremium)
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
private fun LargeWidgetContent(
|
|
tasks: List<WidgetTaskDto>,
|
|
stats: WidgetStats,
|
|
isPremium: Boolean
|
|
) {
|
|
val openApp = actionRunCallback<OpenAppAction>()
|
|
|
|
Box(
|
|
modifier = GlanceModifier
|
|
.fillMaxSize()
|
|
.background(WidgetColors.BACKGROUND_PRIMARY)
|
|
.padding(14.dp)
|
|
.clickable(openApp)
|
|
) {
|
|
if (!isPremium) {
|
|
Column(
|
|
modifier = GlanceModifier.fillMaxSize(),
|
|
horizontalAlignment = Alignment.CenterHorizontally,
|
|
verticalAlignment = Alignment.CenterVertically
|
|
) {
|
|
TaskCountBlock(count = tasks.size, long = true)
|
|
}
|
|
} else {
|
|
Column(modifier = GlanceModifier.fillMaxSize()) {
|
|
WidgetHeader(taskCount = tasks.size, onTap = openApp)
|
|
|
|
Spacer(modifier = GlanceModifier.height(10.dp))
|
|
|
|
if (tasks.isEmpty()) {
|
|
Box(
|
|
modifier = GlanceModifier.defaultWeight().fillMaxWidth(),
|
|
contentAlignment = Alignment.Center
|
|
) {
|
|
EmptyState(compact = false, onTap = openApp)
|
|
}
|
|
} else {
|
|
val shown = tasks.take(MAX_TASKS)
|
|
shown.forEachIndexed { index, task ->
|
|
TaskRow(
|
|
task = task,
|
|
compact = false,
|
|
showResidence = true,
|
|
onTaskClick = openApp,
|
|
trailing = { CompleteButton(taskId = task.id) }
|
|
)
|
|
if (index < shown.lastIndex) {
|
|
Spacer(modifier = GlanceModifier.height(4.dp))
|
|
}
|
|
}
|
|
if (tasks.size > MAX_TASKS) {
|
|
Spacer(modifier = GlanceModifier.height(4.dp))
|
|
Text(
|
|
text = "+ ${tasks.size - MAX_TASKS} more",
|
|
style = TextStyle(
|
|
color = WidgetColors.textSecondary,
|
|
fontSize = 10.sp,
|
|
fontWeight = FontWeight.Medium
|
|
),
|
|
modifier = GlanceModifier.fillMaxWidth()
|
|
)
|
|
}
|
|
Spacer(modifier = GlanceModifier.defaultWeight())
|
|
}
|
|
|
|
Spacer(modifier = GlanceModifier.height(10.dp))
|
|
StatsRow(stats = stats)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
companion object {
|
|
private const val MAX_TASKS = 5
|
|
}
|
|
}
|
|
|
|
/** AppWidget receiver for the large widget. */
|
|
class HoneyDueLargeWidgetReceiver : GlanceAppWidgetReceiver() {
|
|
override val glanceAppWidget: GlanceAppWidget = HoneyDueLargeWidget()
|
|
}
|