- 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>
253 lines
8.8 KiB
Kotlin
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()
|
|
}
|