Files
honeyDueKMP/composeApp/src/androidMain/kotlin/com/tt/honeyDue/widget/HoneyDueMediumWidget.kt
Trey t d3b6b14e78 Fix build failures from rebrand: restore pbxproj exceptions, fix Kotlin casing, move missed source dirs
- Restore 6 missing PBXFileSystemSynchronizedBuildFileExceptionSet entries
  and exceptions arrays on 5 root groups (lost during sed rename)
- Rename extension WidgetIconView.swift to avoid stringsdata collision
  (original had different names: MyCribIconView vs CaseraIconView)
- Rename CaseraExtension.entitlements → HoneyDueExtension.entitlements
- Fix Kotlin object casing: honeyDueShareCodec → HoneyDueShareCodec,
  honeyDuePackageType → HoneyDuePackageType
- Move missed Kotlin source dirs (jsMain, webMain, androidMain/com/casera)
  to com/tt/honeyDue
- Rename remaining Casera widget files to HoneyDue
- Rename CaseraTests.swift → HoneyDueTests.swift

All 4 projects (Go API, iOS, Android, Web) now compile clean.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 06:58:56 -06:00

253 lines
8.8 KiB
Kotlin

package com.tt.honeyDue.widget
import android.content.Context
import android.content.Intent
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.glance.GlanceId
import androidx.glance.GlanceModifier
import androidx.glance.GlanceTheme
import androidx.glance.action.ActionParameters
import androidx.glance.action.actionParametersOf
import androidx.glance.action.clickable
import androidx.glance.appwidget.GlanceAppWidget
import androidx.glance.appwidget.GlanceAppWidgetReceiver
import androidx.glance.appwidget.action.ActionCallback
import androidx.glance.appwidget.action.actionRunCallback
import androidx.glance.appwidget.lazy.LazyColumn
import androidx.glance.appwidget.lazy.items
import androidx.glance.appwidget.provideContent
import androidx.glance.background
import androidx.glance.currentState
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.fillMaxSize
import androidx.glance.layout.fillMaxWidth
import androidx.glance.layout.height
import androidx.glance.layout.padding
import androidx.glance.layout.width
import androidx.glance.state.GlanceStateDefinition
import androidx.glance.state.PreferencesGlanceStateDefinition
import androidx.glance.text.FontWeight
import androidx.glance.text.Text
import androidx.glance.text.TextStyle
import androidx.glance.unit.ColorProvider
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
import kotlinx.serialization.json.Json
/**
* Medium widget showing a list of upcoming tasks
* Size: 4x2
*/
class HoneyDueMediumWidget : GlanceAppWidget() {
override val stateDefinition: GlanceStateDefinition<*> = PreferencesGlanceStateDefinition
private val json = Json { ignoreUnknownKeys = true }
override suspend fun provideGlance(context: Context, id: GlanceId) {
provideContent {
GlanceTheme {
MediumWidgetContent()
}
}
}
@Composable
private fun MediumWidgetContent() {
val prefs = currentState<Preferences>()
val overdueCount = prefs[intPreferencesKey("overdue_count")] ?: 0
val dueSoonCount = prefs[intPreferencesKey("due_soon_count")] ?: 0
val tasksJson = prefs[stringPreferencesKey("tasks_json")] ?: "[]"
val tasks = try {
json.decodeFromString<List<WidgetTask>>(tasksJson).take(5)
} catch (e: Exception) {
emptyList()
}
Box(
modifier = GlanceModifier
.fillMaxSize()
.background(Color(0xFFFFF8E7)) // Cream background
.padding(12.dp)
) {
Column(
modifier = GlanceModifier.fillMaxSize()
) {
// Header
Row(
modifier = GlanceModifier
.fillMaxWidth()
.clickable(actionRunCallback<OpenAppAction>()),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "honeyDue",
style = TextStyle(
color = ColorProvider(Color(0xFF07A0C3)),
fontSize = 18.sp,
fontWeight = FontWeight.Bold
)
)
Spacer(modifier = GlanceModifier.width(8.dp))
// Badge for overdue
if (overdueCount > 0) {
Box(
modifier = GlanceModifier
.background(Color(0xFFDD1C1A))
.padding(horizontal = 6.dp, vertical = 2.dp),
contentAlignment = Alignment.Center
) {
Text(
text = "$overdueCount overdue",
style = TextStyle(
color = ColorProvider(Color.White),
fontSize = 10.sp,
fontWeight = FontWeight.Medium
)
)
}
}
}
Spacer(modifier = GlanceModifier.height(8.dp))
// Task list
if (tasks.isEmpty()) {
Box(
modifier = GlanceModifier
.fillMaxSize()
.clickable(actionRunCallback<OpenAppAction>()),
contentAlignment = Alignment.Center
) {
Text(
text = "No upcoming tasks",
style = TextStyle(
color = ColorProvider(Color(0xFF888888)),
fontSize = 14.sp
)
)
}
} else {
LazyColumn(
modifier = GlanceModifier.fillMaxSize()
) {
items(tasks) { task ->
TaskListItem(task = task)
}
}
}
}
}
}
@Composable
private fun TaskListItem(task: WidgetTask) {
val taskIdKey = ActionParameters.Key<Int>("task_id")
Row(
modifier = GlanceModifier
.fillMaxWidth()
.padding(vertical = 4.dp)
.clickable(
actionRunCallback<OpenTaskAction>(
actionParametersOf(taskIdKey to task.id)
)
),
verticalAlignment = Alignment.CenterVertically
) {
// Priority indicator
Box(
modifier = GlanceModifier
.width(4.dp)
.height(32.dp)
.background(getPriorityColor(task.priorityLevel))
) {}
Spacer(modifier = GlanceModifier.width(8.dp))
Column(
modifier = GlanceModifier.fillMaxWidth()
) {
Text(
text = task.title,
style = TextStyle(
color = ColorProvider(Color(0xFF1A1A1A)),
fontSize = 13.sp,
fontWeight = FontWeight.Medium
),
maxLines = 1
)
Row {
Text(
text = task.residenceName,
style = TextStyle(
color = ColorProvider(Color(0xFF666666)),
fontSize = 11.sp
),
maxLines = 1
)
if (task.dueDate != null) {
Text(
text = "${task.dueDate}",
style = TextStyle(
color = ColorProvider(
if (task.isOverdue) Color(0xFFDD1C1A) else Color(0xFF666666)
),
fontSize = 11.sp
)
)
}
}
}
}
}
private fun getPriorityColor(level: Int): Color {
return when (level) {
4 -> Color(0xFFDD1C1A) // Urgent - Red
3 -> Color(0xFFF5A623) // High - Amber
2 -> Color(0xFF07A0C3) // Medium - Primary
else -> Color(0xFF888888) // Low - Gray
}
}
}
/**
* Action to open a specific task
*/
class OpenTaskAction : ActionCallback {
override suspend fun onAction(
context: Context,
glanceId: GlanceId,
parameters: ActionParameters
) {
val taskId = parameters[ActionParameters.Key<Int>("task_id")]
val intent = context.packageManager.getLaunchIntentForPackage(context.packageName)
intent?.let {
it.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
if (taskId != null) {
it.putExtra("navigate_to_task", taskId)
}
context.startActivity(it)
}
}
}
/**
* Receiver for the medium widget
*/
class HoneyDueMediumWidgetReceiver : GlanceAppWidgetReceiver() {
override val glanceAppWidget: GlanceAppWidget = HoneyDueMediumWidget()
}