diff options
| author | Peter Stone <thepeterstone@gmail.com> | 2026-06-29 08:08:47 +0000 |
|---|---|---|
| committer | Peter Stone <thepeterstone@gmail.com> | 2026-06-29 08:08:47 +0000 |
| commit | de3e37cca4f34a30f9c0beef05f67c3d515d64c2 (patch) | |
| tree | 558da8786b5a7a4a07ebe8cd77785844ac669d24 /android/app/src/main/java/org/terst/doot | |
| parent | dacb710fc4ea63baa05cc47016def3849b9db5fa (diff) | |
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 <noreply@anthropic.com>
Diffstat (limited to 'android/app/src/main/java/org/terst/doot')
| -rw-r--r-- | android/app/src/main/java/org/terst/doot/widget/ui/DootWidget.kt | 39 | ||||
| -rw-r--r-- | android/app/src/main/java/org/terst/doot/widget/ui/TaskDetailActivity.kt | 110 |
2 files changed, 129 insertions, 20 deletions
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 dcb73d5..72fcccc 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 @@ -46,10 +46,9 @@ class DootWidget : GlanceAppWidget() { val items = parseItems(prefs) val now = prefs[Keys.NOW]?.let { runCatching { Instant.parse(it) }.getOrNull() } ?: Instant.now() - val serverUrl = prefs[Keys.SERVER_URL]?.trimEnd('/') ?: "" provideContent { - WidgetRoot(items, now, serverUrl) + WidgetRoot(items, now) } } @@ -60,7 +59,7 @@ class DootWidget : GlanceAppWidget() { } @Composable -fun WidgetRoot(items: List<WidgetItem>, now: Instant, serverUrl: String = "") { +fun WidgetRoot(items: List<WidgetItem>, 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<WidgetItem>, 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<WidgetItem>, fragments: List<TaskFragment>, - @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)) + } + } +} |
