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 | |
| 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')
4 files changed, 146 insertions, 20 deletions
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"> + <!-- Task detail bottom sheet (opened by widget task taps) --> + <activity + android:name=".ui.TaskDetailActivity" + android:theme="@style/Theme.TaskDetail" + android:exported="false" /> + <!-- Settings activity (also the widget config screen) --> <activity android:name=".ui.SettingsActivity" 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)) + } + } +} 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 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Transparent window for TaskDetailActivity so the launcher shows behind the bottom sheet --> + <style name="Theme.TaskDetail" parent="android:Theme.DeviceDefault.NoActionBar"> + <item name="android:windowIsTranslucent">true</item> + <item name="android:windowBackground">@android:color/transparent</item> + <item name="android:windowIsFloating">false</item> + <item name="android:windowNoTitle">true</item> + <item name="android:statusBarColor">@android:color/transparent</item> + </style> +</resources> |
