From dacb710fc4ea63baa05cc47016def3849b9db5fa Mon Sep 17 00:00:00 2001 From: Peter Stone Date: Mon, 29 Jun 2026 07:09:43 +0000 Subject: feat: add standalone task detail page for Android widget deep-links Adds GET /task?id=xxx&source=xxx route that renders a full mobile-friendly task detail page (session-protected). Widget task rows now open this page when tapped. HandleUpdateTask redirects back to the page after a non-HTMX save. Android: threads serverUrl through composable chain to TaskRow. Co-Authored-By: Claude Sonnet 4.6 --- .../java/org/terst/doot/widget/ui/DootWidget.kt | 32 ++++++++---- cmd/dashboard/main.go | 1 + internal/handlers/handlers.go | 58 +++++++++++++++++++++- web/templates/task-detail-page.html | 30 +++++++++++ 4 files changed, 109 insertions(+), 12 deletions(-) create mode 100644 web/templates/task-detail-page.html 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 f8d8ae1..dcb73d5 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,9 +46,10 @@ 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) + WidgetRoot(items, now, serverUrl) } } @@ -59,7 +60,7 @@ class DootWidget : GlanceAppWidget() { } @Composable -fun WidgetRoot(items: List, now: Instant) { +fun WidgetRoot(items: List, now: Instant, serverUrl: String = "") { val zone = ZoneId.systemDefault() val nowZoned: ZonedDateTime = now.atZone(zone) val allScheduled = items.filter { it.start != null }.sortedBy { Instant.parse(it.start!!) } @@ -89,7 +90,7 @@ fun WidgetRoot(items: List, now: Instant) { } for (hour in gridStart..gridEnd) { - HourRow(hour, nowZoned, scheduledEvents, fragments, zone) + HourRow(hour, nowZoned, scheduledEvents, fragments, zone, serverUrl) } } } @@ -100,7 +101,8 @@ fun HourRow( nowZoned: ZonedDateTime, scheduled: List, fragments: List, - @Suppress("UNUSED_PARAMETER") zone: ZoneId + @Suppress("UNUSED_PARAMETER") zone: ZoneId, + serverUrl: String = "" ) { val hourStart = nowZoned.withHour(hour).withMinute(0).withSecond(0).withNano(0).toInstant() val hourEnd = hourStart.plus(1, ChronoUnit.HOURS) @@ -147,7 +149,7 @@ fun HourRow( } fragsHere.forEach { frag -> - TaskFragmentBlock(frag) + TaskFragmentBlock(frag, serverUrl) } } } @@ -183,7 +185,7 @@ fun EventBlock(event: WidgetItem, isPast: Boolean) { } @Composable -fun TaskFragmentBlock(fragment: TaskFragment) { +fun TaskFragmentBlock(fragment: TaskFragment, serverUrl: String = "") { Column( modifier = GlanceModifier .fillMaxWidth() @@ -199,17 +201,25 @@ fun TaskFragmentBlock(fragment: TaskFragment) { modifier = GlanceModifier.padding(start = 8.dp, top = 3.dp, bottom = 1.dp) ) fragment.slots.forEach { slot -> - TaskRow(slot.task) + TaskRow(slot.task, serverUrl) } } } @Composable -fun TaskRow(task: WidgetItem) { +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 + } + val rowModifier = GlanceModifier + .fillMaxWidth() + .padding(horizontal = 8.dp, vertical = 5.dp) + .let { if (tapAction != null) it.clickable(tapAction) else it } Row( - modifier = GlanceModifier - .fillMaxWidth() - .padding(horizontal = 8.dp, vertical = 5.dp), + modifier = rowModifier, verticalAlignment = Alignment.CenterVertically ) { if (task.completable) { diff --git a/cmd/dashboard/main.go b/cmd/dashboard/main.go index 133b684..92a149b 100644 --- a/cmd/dashboard/main.go +++ b/cmd/dashboard/main.go @@ -324,6 +324,7 @@ func main() { // Task detail/edit r.Get("/tasks/detail", h.HandleGetTaskDetail) + r.Get("/task", h.HandleTaskDetailPage) r.Post("/tasks/update", h.HandleUpdateTask) // Shopping quick-add diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index 19d26be..0903cf3 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -814,6 +814,58 @@ func (h *Handler) HandleGetTaskDetail(w http.ResponseWriter, r *http.Request) { }{title, id, source, description}) } +// HandleTaskDetailPage renders a standalone full-page task detail view (used by Android widget deep-links). +func (h *Handler) HandleTaskDetailPage(w http.ResponseWriter, r *http.Request) { + id := r.URL.Query().Get("id") + source := r.URL.Query().Get("source") + + if id == "" || source == "" { + http.Error(w, "Missing id or source", http.StatusBadRequest) + return + } + + var title, description string + switch source { + case "todoist": + if tasks, err := h.store.GetTasks(); err == nil { + for _, t := range tasks { + if t.ID == id { + title, description = t.Content, t.Description + break + } + } + } + case "trello": + if boards, err := h.store.GetBoards(); err == nil { + for _, b := range boards { + for _, c := range b.Cards { + if c.ID == id { + title = c.Name + break + } + } + } + } + } + + if title == "" { + title = "Task" + } + + data := struct { + Title string + ID string + Source string + Description string + CSRFToken string + Saved bool + }{title, id, source, description, auth.GetCSRFTokenFromContext(r.Context()), r.URL.Query().Get("saved") == "1"} + + if err := h.renderer.Render(w, "task-detail-page.html", data); err != nil { + http.Error(w, "Failed to render template", http.StatusInternalServerError) + } +} + // HandleUpdateTask updates a task description func (h *Handler) HandleUpdateTask(w http.ResponseWriter, r *http.Request) { if err := r.ParseForm(); err != nil { @@ -846,7 +898,11 @@ func (h *Handler) HandleUpdateTask(w http.ResponseWriter, r *http.Request) { return } - w.WriteHeader(http.StatusOK) + if r.Header.Get("HX-Request") != "" { + w.WriteHeader(http.StatusOK) + } else { + http.Redirect(w, r, "/task?id="+id+"&source="+source+"&saved=1", http.StatusSeeOther) + } } // HandleTabTasks renders the unified Tasks tab (Todoist + Trello cards with due dates + Bugs + Google Tasks) diff --git a/web/templates/task-detail-page.html b/web/templates/task-detail-page.html new file mode 100644 index 0000000..9bbf008 --- /dev/null +++ b/web/templates/task-detail-page.html @@ -0,0 +1,30 @@ + + + + + + {{.Title}} + + + +
+ ← Dashboard + {{if .Saved}}

Saved.

{{end}} +
+

{{.Title}}

+ {{if eq .Source "todoist"}} +
+ + + + + + +
+ {{else}} +

Source: {{.Source}}

+ {{end}} +
+
+ + -- cgit v1.2.3