diff options
| -rw-r--r-- | android/app/src/main/java/org/terst/doot/widget/data/WidgetRepository.kt | 16 | ||||
| -rw-r--r-- | android/app/src/main/java/org/terst/doot/widget/ui/TaskDetailActivity.kt | 70 | ||||
| -rw-r--r-- | cmd/dashboard/main.go | 1 | ||||
| -rw-r--r-- | internal/handlers/widget.go | 31 | ||||
| -rw-r--r-- | internal/store/native_tasks.go | 8 |
5 files changed, 124 insertions, 2 deletions
diff --git a/android/app/src/main/java/org/terst/doot/widget/data/WidgetRepository.kt b/android/app/src/main/java/org/terst/doot/widget/data/WidgetRepository.kt index 6f3d2b2..a54910d 100644 --- a/android/app/src/main/java/org/terst/doot/widget/data/WidgetRepository.kt +++ b/android/app/src/main/java/org/terst/doot/widget/data/WidgetRepository.kt @@ -43,6 +43,22 @@ class WidgetRepository( } } + /** POSTs a due-date update to /api/widget/reschedule. */ + suspend fun reschedule(id: String, source: String, dateISO: String): Result<Unit> = + withContext(Dispatchers.IO) { + val body = """{"id":"$id","source":"$source","date":"$dateISO"}""" + .toRequestBody("application/json".toMediaType()) + val request = Request.Builder() + .url("$serverUrl/api/widget/reschedule") + .header("Authorization", "Bearer $token") + .post(body) + .build() + runCatching { + val response = client.newCall(request).execute() + check(response.isSuccessful) { "HTTP ${response.code}" } + } + } + /** POSTs a task completion to /api/widget/complete. */ suspend fun complete(context: Context, id: String, source: String): Result<Unit> = withContext(Dispatchers.IO) { 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 index 2a42871..0dac1cc 100644 --- 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 @@ -7,13 +7,23 @@ 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.runtime.* 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 androidx.glance.appwidget.updateAll +import androidx.lifecycle.lifecycleScope +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import okhttp3.OkHttpClient +import org.terst.doot.widget.data.Keys +import org.terst.doot.widget.data.WidgetRepository +import org.terst.doot.widget.data.dataStore import org.terst.doot.widget.work.CompleteWorker +import java.util.Calendar +import java.util.TimeZone class TaskDetailActivity : ComponentActivity() { @@ -35,6 +45,19 @@ class TaskDetailActivity : ComponentActivity() { CompleteWorker.enqueue(this, id, source) finish() }, + onReschedule = { dateISO -> + lifecycleScope.launch { + val prefs = this@TaskDetailActivity.dataStore.data.first() + val url = prefs[Keys.SERVER_URL]?.trimEnd('/') ?: return@launch + val token = prefs[Keys.TOKEN] ?: return@launch + val repo = WidgetRepository(OkHttpClient(), url, token) + repo.reschedule(id, source, dateISO).onSuccess { + repo.fetchAndPersist(this@TaskDetailActivity) + DootWidget().updateAll(this@TaskDetailActivity) + finish() + } + } + }, onDismiss = ::finish ) } @@ -56,8 +79,40 @@ fun TaskDetailSheet( source: String, completable: Boolean, onComplete: () -> Unit, + onReschedule: (String) -> Unit, onDismiss: () -> Unit ) { + var showDatePicker by remember { mutableStateOf(false) } + val datePickerState = rememberDatePickerState( + initialSelectedDateMillis = System.currentTimeMillis() + ) + + if (showDatePicker) { + DatePickerDialog( + onDismissRequest = { showDatePicker = false }, + confirmButton = { + TextButton(onClick = { + datePickerState.selectedDateMillis?.let { millis -> + val cal = Calendar.getInstance(TimeZone.getTimeZone("UTC")) + cal.timeInMillis = millis + val dateISO = "%04d-%02d-%02d".format( + cal.get(Calendar.YEAR), + cal.get(Calendar.MONTH) + 1, + cal.get(Calendar.DAY_OF_MONTH) + ) + onReschedule(dateISO) + } + showDatePicker = false + }) { Text("Set date") } + }, + dismissButton = { + TextButton(onClick = { showDatePicker = false }) { Text("Cancel") } + } + ) { + DatePicker(state = datePickerState) + } + } + ModalBottomSheet( onDismissRequest = onDismiss, sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true), @@ -94,7 +149,7 @@ fun TaskDetailSheet( modifier = Modifier.weight(1f) ) } - Spacer(Modifier.height(24.dp)) + Spacer(Modifier.height(20.dp)) if (completable) { Button( onClick = onComplete, @@ -103,6 +158,17 @@ fun TaskDetailSheet( ) { Text("Mark Complete", fontSize = 15.sp) } + Spacer(Modifier.height(8.dp)) + } + if (source == "doot") { + OutlinedButton( + onClick = { showDatePicker = true }, + modifier = Modifier.fillMaxWidth(), + colors = ButtonDefaults.outlinedButtonColors(contentColor = Color.White), + border = androidx.compose.foundation.BorderStroke(1.dp, Color.White.copy(alpha = 0.3f)) + ) { + Text("Reschedule", fontSize = 15.sp) + } } Spacer(Modifier.height(20.dp)) } diff --git a/cmd/dashboard/main.go b/cmd/dashboard/main.go index c154d9b..9f69d46 100644 --- a/cmd/dashboard/main.go +++ b/cmd/dashboard/main.go @@ -366,6 +366,7 @@ func main() { } r.With(widgetAuth).Get("/api/widget", h.HandleWidgetGet) r.With(widgetAuth).Post("/api/widget/complete", h.HandleWidgetComplete) + r.With(widgetAuth).Post("/api/widget/reschedule", h.HandleWidgetReschedule) } else { log.Println("WIDGET_TOKEN not set — /api/widget disabled") } diff --git a/internal/handlers/widget.go b/internal/handlers/widget.go index 360ccc5..ee13dc4 100644 --- a/internal/handlers/widget.go +++ b/internal/handlers/widget.go @@ -101,6 +101,37 @@ type widgetCompleteRequest struct { Source string `json:"source"` } +type widgetRescheduleRequest struct { + ID string `json:"id"` + Source string `json:"source"` + Date string `json:"date"` // YYYY-MM-DD +} + +// HandleWidgetReschedule updates the due date of a native task. +func (h *Handler) HandleWidgetReschedule(w http.ResponseWriter, r *http.Request) { + var req widgetRescheduleRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "bad request", http.StatusBadRequest) + return + } + if req.Source != "doot" { + http.Error(w, "only doot tasks can be rescheduled via widget", http.StatusBadRequest) + return + } + parsed, err := time.Parse("2006-01-02", req.Date) + if err != nil { + http.Error(w, "invalid date", http.StatusBadRequest) + return + } + tz := config.GetDisplayTimezone() + dueDate := time.Date(parsed.Year(), parsed.Month(), parsed.Day(), 0, 0, 0, 0, tz) + if err := h.store.RescheduleNativeTask(req.ID, dueDate); err != nil { + http.Error(w, "failed to reschedule", http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusOK) +} + // HandleWidgetComplete proxies a task completion to the source API or native store. func (h *Handler) HandleWidgetComplete(w http.ResponseWriter, r *http.Request) { var req widgetCompleteRequest diff --git a/internal/store/native_tasks.go b/internal/store/native_tasks.go index 3ec043b..ca19377 100644 --- a/internal/store/native_tasks.go +++ b/internal/store/native_tasks.go @@ -80,6 +80,14 @@ func (s *Store) CompleteNativeTask(id string) error { return err } +// RescheduleNativeTask sets a new due date on a task. +func (s *Store) RescheduleNativeTask(id string, dueDate time.Time) error { + _, err := s.db.Exec(` + UPDATE native_tasks SET due_date = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? + `, dueDate, id) + return err +} + // UncompleteNativeTask marks a task as not completed. func (s *Store) UncompleteNativeTask(id string) error { _, err := s.db.Exec(` |
