summaryrefslogtreecommitdiff
path: root/android/app/src/main/java/org/terst/doot
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-06-29 08:08:47 +0000
committerPeter Stone <thepeterstone@gmail.com>2026-06-29 08:08:47 +0000
commitde3e37cca4f34a30f9c0beef05f67c3d515d64c2 (patch)
tree558da8786b5a7a4a07ebe8cd77785844ac669d24 /android/app/src/main/java/org/terst/doot
parentdacb710fc4ea63baa05cc47016def3849b9db5fa (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.kt39
-rw-r--r--android/app/src/main/java/org/terst/doot/widget/ui/TaskDetailActivity.kt110
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))
+ }
+ }
+}