From 029902606396cf5ec583e187e7adc576c4aafb71 Mon Sep 17 00:00:00 2001 From: Peter Stone Date: Sun, 12 Jul 2026 10:38:59 +0000 Subject: feat(widget): add quick-add button and entry sheet --- .../org/terst/doot/widget/data/WidgetRepository.kt | 26 +++++ .../java/org/terst/doot/widget/ui/DootWidget.kt | 23 +++++ .../org/terst/doot/widget/ui/QuickAddActivity.kt | 114 +++++++++++++++++++++ 3 files changed, 163 insertions(+) create mode 100644 android/app/src/main/java/org/terst/doot/widget/ui/QuickAddActivity.kt (limited to 'android/app/src/main/java/org/terst') diff --git a/android/app/src/main/java/org/terst/doot/widget/data/WidgetRepository.kt b/android/app/src/main/java/org/terst/doot/widget/data/WidgetRepository.kt index a54910d..39f2a55 100644 --- a/android/app/src/main/java/org/terst/doot/widget/data/WidgetRepository.kt +++ b/android/app/src/main/java/org/terst/doot/widget/data/WidgetRepository.kt @@ -4,6 +4,7 @@ import android.content.Context import androidx.datastore.preferences.core.edit import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import kotlinx.serialization.Serializable import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import okhttp3.MediaType.Companion.toMediaType @@ -13,6 +14,9 @@ import okhttp3.RequestBody.Companion.toRequestBody private val json = Json { ignoreUnknownKeys = true } +@Serializable +private data class WidgetAddRequest(val title: String) + class WidgetRepository( private val client: OkHttpClient, private val serverUrl: String, @@ -74,4 +78,26 @@ class WidgetRepository( check(response.isSuccessful) { "HTTP ${response.code}" } } } + + /** + * POSTs a new task title to /api/widget/add. Uses proper JSON encoding + * (not manual string interpolation like reschedule/complete above) because + * the title is arbitrary user text that could contain characters that + * break hand-built JSON -- id/source above are safe because they're + * internal identifiers, never user-typed free text. + */ + suspend fun addTask(title: String): Result = + withContext(Dispatchers.IO) { + val body = json.encodeToString(WidgetAddRequest(title)) + .toRequestBody("application/json".toMediaType()) + val request = Request.Builder() + .url("$serverUrl/api/widget/add") + .header("Authorization", "Bearer $token") + .post(body) + .build() + runCatching { + val response = client.newCall(request).execute() + check(response.isSuccessful) { "HTTP ${response.code}" } + } + } } diff --git a/android/app/src/main/java/org/terst/doot/widget/ui/DootWidget.kt b/android/app/src/main/java/org/terst/doot/widget/ui/DootWidget.kt index a0adf85..e326537 100644 --- a/android/app/src/main/java/org/terst/doot/widget/ui/DootWidget.kt +++ b/android/app/src/main/java/org/terst/doot/widget/ui/DootWidget.kt @@ -121,6 +121,8 @@ fun WidgetRoot(items: List, now: Instant, isRefreshing: Boolean) { ), modifier = GlanceModifier.defaultWeight() ) + QuickAddButton() + Spacer(modifier = GlanceModifier.width(4.dp)) RefreshButton(isRefreshing) } @@ -176,6 +178,27 @@ fun RefreshButton(isRefreshing: Boolean) { } } +@Composable +fun QuickAddButton() { + val context = LocalContext.current + val intent = Intent(context, QuickAddActivity::class.java).apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + Box( + modifier = GlanceModifier + .size(24.dp) + .clickable(actionStartActivity(intent)), + contentAlignment = Alignment.Center + ) { + Image( + provider = ImageProvider(org.terst.doot.widget.R.drawable.ic_add), + contentDescription = "Add task", + colorFilter = ColorFilter.tint(ColorProvider(Color(0x99FFFFFF))), + modifier = GlanceModifier.size(14.dp) + ) + } +} + @Composable fun HourRow( hour: Int, diff --git a/android/app/src/main/java/org/terst/doot/widget/ui/QuickAddActivity.kt b/android/app/src/main/java/org/terst/doot/widget/ui/QuickAddActivity.kt new file mode 100644 index 0000000..52933ea --- /dev/null +++ b/android/app/src/main/java/org/terst/doot/widget/ui/QuickAddActivity.kt @@ -0,0 +1,114 @@ +package org.terst.doot.widget.ui + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.glance.appwidget.updateAll +import androidx.lifecycle.lifecycleScope +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import okhttp3.OkHttpClient +import org.terst.doot.widget.data.Keys +import org.terst.doot.widget.data.WidgetRepository +import org.terst.doot.widget.data.dataStore + +class QuickAddActivity : ComponentActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContent { + MaterialTheme(colorScheme = darkColorScheme()) { + QuickAddSheet( + onAdd = { title -> + lifecycleScope.launch { + val prefs = this@QuickAddActivity.dataStore.data.first() + val url = prefs[Keys.SERVER_URL]?.trimEnd('/') ?: return@launch + val token = prefs[Keys.TOKEN] ?: return@launch + val repo = WidgetRepository(OkHttpClient(), url, token) + repo.addTask(title).onSuccess { + repo.fetchAndPersist(this@QuickAddActivity) + DootWidget().updateAll(this@QuickAddActivity) + finish() + } + } + }, + onDismiss = ::finish + ) + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun QuickAddSheet( + onAdd: (String) -> Unit, + onDismiss: () -> Unit +) { + var title by remember { mutableStateOf("") } + + ModalBottomSheet( + onDismissRequest = onDismiss, + sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true), + containerColor = Color(0xFF1E293B), + tonalElevation = 0.dp, + dragHandle = { + Box( + modifier = Modifier + .padding(vertical = 12.dp) + .width(36.dp) + .height(4.dp) + .background(Color.White.copy(alpha = 0.25f), RoundedCornerShape(2.dp)) + ) + } + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp) + .navigationBarsPadding() + ) { + Text( + text = "Quick Add", + fontSize = 17.sp, + fontWeight = FontWeight.SemiBold, + color = Color.White + ) + Spacer(Modifier.height(16.dp)) + OutlinedTextField( + value = title, + onValueChange = { title = it }, + placeholder = { Text("Task title") }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + colors = OutlinedTextFieldDefaults.colors( + focusedTextColor = Color.White, + unfocusedTextColor = Color.White, + focusedBorderColor = Color(0xFF3B82F6), + unfocusedBorderColor = Color.White.copy(alpha = 0.3f) + ) + ) + Spacer(Modifier.height(16.dp)) + Button( + onClick = { onAdd(title.trim()) }, + enabled = title.isNotBlank(), + modifier = Modifier.fillMaxWidth(), + colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF3B82F6)) + ) { + Text("Add", fontSize = 15.sp) + } + Spacer(Modifier.height(20.dp)) + } + } +} -- cgit v1.2.3