From de3e37cca4f34a30f9c0beef05f67c3d515d64c2 Mon Sep 17 00:00:00 2001 From: Peter Stone Date: Mon, 29 Jun 2026 08:08:47 +0000 Subject: feat: native bottom sheet for widget task detail Replaces browser deep-link with a transparent TaskDetailActivity that shows a Material3 ModalBottomSheet (20-40% screen height). The launcher shows through the transparent window behind the dark scrim. Sheet shows source color dot, task title, and Mark Complete button for Todoist tasks. Tapping outside or swiping down dismisses. No browser involved. Co-Authored-By: Claude Sonnet 4.6 --- android/app/src/main/AndroidManifest.xml | 6 ++ .../java/org/terst/doot/widget/ui/DootWidget.kt | 39 ++++---- .../org/terst/doot/widget/ui/TaskDetailActivity.kt | 110 +++++++++++++++++++++ android/app/src/main/res/values/themes.xml | 11 +++ 4 files changed, 146 insertions(+), 20 deletions(-) create mode 100644 android/app/src/main/java/org/terst/doot/widget/ui/TaskDetailActivity.kt create mode 100644 android/app/src/main/res/values/themes.xml (limited to 'android/app/src/main') diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index f6bf015..3fc5603 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -10,6 +10,12 @@ android:theme="@android:style/Theme.DeviceDefault.DayNight" android:allowBackup="false"> + + + , now: Instant, serverUrl: String = "") { +fun WidgetRoot(items: List, now: Instant) { val zone = ZoneId.systemDefault() val nowZoned: ZonedDateTime = now.atZone(zone) val allScheduled = items.filter { it.start != null }.sortedBy { Instant.parse(it.start!!) } @@ -90,7 +89,7 @@ fun WidgetRoot(items: List, now: Instant, serverUrl: String = "") { } for (hour in gridStart..gridEnd) { - HourRow(hour, nowZoned, scheduledEvents, fragments, zone, serverUrl) + HourRow(hour, nowZoned, scheduledEvents, fragments, zone) } } } @@ -101,8 +100,7 @@ fun HourRow( nowZoned: ZonedDateTime, scheduled: List, fragments: List, - @Suppress("UNUSED_PARAMETER") zone: ZoneId, - serverUrl: String = "" + @Suppress("UNUSED_PARAMETER") zone: ZoneId ) { val hourStart = nowZoned.withHour(hour).withMinute(0).withSecond(0).withNano(0).toInstant() val hourEnd = hourStart.plus(1, ChronoUnit.HOURS) @@ -149,7 +147,7 @@ fun HourRow( } fragsHere.forEach { frag -> - TaskFragmentBlock(frag, serverUrl) + TaskFragmentBlock(frag) } } } @@ -185,7 +183,7 @@ fun EventBlock(event: WidgetItem, isPast: Boolean) { } @Composable -fun TaskFragmentBlock(fragment: TaskFragment, serverUrl: String = "") { +fun TaskFragmentBlock(fragment: TaskFragment) { Column( modifier = GlanceModifier .fillMaxWidth() @@ -201,25 +199,26 @@ fun TaskFragmentBlock(fragment: TaskFragment, serverUrl: String = "") { modifier = GlanceModifier.padding(start = 8.dp, top = 3.dp, bottom = 1.dp) ) fragment.slots.forEach { slot -> - TaskRow(slot.task, serverUrl) + TaskRow(slot.task) } } } @Composable -fun TaskRow(task: WidgetItem, serverUrl: String = "") { - val tapAction = if (serverUrl.isNotEmpty()) { - val url = "$serverUrl/task?id=${task.id}&source=${task.source}" - actionStartActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url))) - } else { - null +fun TaskRow(task: WidgetItem) { + val context = LocalContext.current + val detailIntent = Intent(context, TaskDetailActivity::class.java).apply { + putExtra(TaskDetailActivity.EXTRA_ID, task.id) + putExtra(TaskDetailActivity.EXTRA_SOURCE, task.source) + putExtra(TaskDetailActivity.EXTRA_TITLE, task.title) + putExtra(TaskDetailActivity.EXTRA_COMPLETABLE, task.completable) + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) } - val rowModifier = GlanceModifier - .fillMaxWidth() - .padding(horizontal = 8.dp, vertical = 5.dp) - .let { if (tapAction != null) it.clickable(tapAction) else it } Row( - modifier = rowModifier, + modifier = GlanceModifier + .fillMaxWidth() + .padding(horizontal = 8.dp, vertical = 5.dp) + .clickable(actionStartActivity(detailIntent)), verticalAlignment = Alignment.CenterVertically ) { if (task.completable) { diff --git a/android/app/src/main/java/org/terst/doot/widget/ui/TaskDetailActivity.kt b/android/app/src/main/java/org/terst/doot/widget/ui/TaskDetailActivity.kt new file mode 100644 index 0000000..2a42871 --- /dev/null +++ b/android/app/src/main/java/org/terst/doot/widget/ui/TaskDetailActivity.kt @@ -0,0 +1,110 @@ +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.Composable +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 org.terst.doot.widget.work.CompleteWorker + +class TaskDetailActivity : ComponentActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val id = intent.getStringExtra(EXTRA_ID) ?: return finish() + val source = intent.getStringExtra(EXTRA_SOURCE) ?: return finish() + val title = intent.getStringExtra(EXTRA_TITLE) ?: "" + val completable = intent.getBooleanExtra(EXTRA_COMPLETABLE, false) + + setContent { + MaterialTheme(colorScheme = darkColorScheme()) { + TaskDetailSheet( + title = title, + source = source, + completable = completable, + onComplete = { + CompleteWorker.enqueue(this, id, source) + finish() + }, + onDismiss = ::finish + ) + } + } + } + + companion object { + const val EXTRA_ID = "task_id" + const val EXTRA_SOURCE = "task_source" + const val EXTRA_TITLE = "task_title" + const val EXTRA_COMPLETABLE = "task_completable" + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun TaskDetailSheet( + title: String, + source: String, + completable: Boolean, + onComplete: () -> Unit, + onDismiss: () -> Unit +) { + 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() + ) { + Row(verticalAlignment = androidx.compose.ui.Alignment.CenterVertically) { + Box( + modifier = Modifier + .size(10.dp) + .background(sourceColor(source), RoundedCornerShape(5.dp)) + ) + Spacer(Modifier.width(10.dp)) + Text( + text = title, + fontSize = 17.sp, + fontWeight = FontWeight.SemiBold, + color = Color.White, + modifier = Modifier.weight(1f) + ) + } + Spacer(Modifier.height(24.dp)) + if (completable) { + Button( + onClick = onComplete, + modifier = Modifier.fillMaxWidth(), + colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF3B82F6)) + ) { + Text("Mark Complete", fontSize = 15.sp) + } + } + Spacer(Modifier.height(20.dp)) + } + } +} diff --git a/android/app/src/main/res/values/themes.xml b/android/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..ef88823 --- /dev/null +++ b/android/app/src/main/res/values/themes.xml @@ -0,0 +1,11 @@ + + + + + -- cgit v1.2.3