- Add platform haptic feedback abstraction (HapticFeedback.kt) with implementations for Android, iOS, JVM, JS, and WASM - Enhance CompleteTaskDialog with interactive 5-star rating, image thumbnails, and haptic feedback - Add ImageBitmap platform abstraction for displaying selected images - Localize TaskTemplatesBrowserSheet with string resources - Add Android widgets infrastructure (small, medium, large sizes) - Add Google Sign-In button components and auth flow preparation - Update strings.xml with new localization keys for completions, templates, and document features - Integrate haptic feedback into ThemePickerDialog 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
172 lines
5.6 KiB
Kotlin
172 lines
5.6 KiB
Kotlin
package com.example.casera.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.Image
|
|
import androidx.glance.ImageProvider
|
|
import androidx.glance.action.ActionParameters
|
|
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.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.size
|
|
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 com.example.casera.R
|
|
|
|
/**
|
|
* Small widget showing task count summary
|
|
* Size: 2x1 or 2x2
|
|
*/
|
|
class CaseraSmallWidget : GlanceAppWidget() {
|
|
|
|
override val stateDefinition: GlanceStateDefinition<*> = PreferencesGlanceStateDefinition
|
|
|
|
override suspend fun provideGlance(context: Context, id: GlanceId) {
|
|
provideContent {
|
|
GlanceTheme {
|
|
SmallWidgetContent()
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
private fun SmallWidgetContent() {
|
|
val prefs = currentState<Preferences>()
|
|
val overdueCount = prefs[intPreferencesKey("overdue_count")] ?: 0
|
|
val dueSoonCount = prefs[intPreferencesKey("due_soon_count")] ?: 0
|
|
val inProgressCount = prefs[intPreferencesKey("in_progress_count")] ?: 0
|
|
|
|
Box(
|
|
modifier = GlanceModifier
|
|
.fillMaxSize()
|
|
.background(Color(0xFFFFF8E7)) // Cream background
|
|
.clickable(actionRunCallback<OpenAppAction>())
|
|
.padding(12.dp),
|
|
contentAlignment = Alignment.Center
|
|
) {
|
|
Column(
|
|
modifier = GlanceModifier.fillMaxWidth(),
|
|
horizontalAlignment = Alignment.CenterHorizontally
|
|
) {
|
|
// App name/logo
|
|
Text(
|
|
text = "Casera",
|
|
style = TextStyle(
|
|
color = ColorProvider(Color(0xFF07A0C3)),
|
|
fontSize = 16.sp,
|
|
fontWeight = FontWeight.Bold
|
|
)
|
|
)
|
|
|
|
Spacer(modifier = GlanceModifier.height(8.dp))
|
|
|
|
// Task counts row
|
|
Row(
|
|
modifier = GlanceModifier.fillMaxWidth(),
|
|
horizontalAlignment = Alignment.CenterHorizontally
|
|
) {
|
|
// Overdue
|
|
TaskCountItem(
|
|
count = overdueCount,
|
|
label = "Overdue",
|
|
color = Color(0xFFDD1C1A) // Red
|
|
)
|
|
|
|
Spacer(modifier = GlanceModifier.width(16.dp))
|
|
|
|
// Due Soon
|
|
TaskCountItem(
|
|
count = dueSoonCount,
|
|
label = "Due Soon",
|
|
color = Color(0xFFF5A623) // Amber
|
|
)
|
|
|
|
Spacer(modifier = GlanceModifier.width(16.dp))
|
|
|
|
// In Progress
|
|
TaskCountItem(
|
|
count = inProgressCount,
|
|
label = "Active",
|
|
color = Color(0xFF07A0C3) // Primary
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
private fun TaskCountItem(count: Int, label: String, color: Color) {
|
|
Column(
|
|
horizontalAlignment = Alignment.CenterHorizontally
|
|
) {
|
|
Text(
|
|
text = count.toString(),
|
|
style = TextStyle(
|
|
color = ColorProvider(color),
|
|
fontSize = 24.sp,
|
|
fontWeight = FontWeight.Bold
|
|
)
|
|
)
|
|
Text(
|
|
text = label,
|
|
style = TextStyle(
|
|
color = ColorProvider(Color(0xFF666666)),
|
|
fontSize = 10.sp
|
|
)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Action to open the main app
|
|
*/
|
|
class OpenAppAction : ActionCallback {
|
|
override suspend fun onAction(
|
|
context: Context,
|
|
glanceId: GlanceId,
|
|
parameters: ActionParameters
|
|
) {
|
|
val intent = context.packageManager.getLaunchIntentForPackage(context.packageName)
|
|
intent?.let {
|
|
it.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
|
context.startActivity(it)
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Receiver for the small widget
|
|
*/
|
|
class CaseraSmallWidgetReceiver : GlanceAppWidgetReceiver() {
|
|
override val glanceAppWidget: GlanceAppWidget = CaseraSmallWidget()
|
|
}
|