summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--android/app/src/main/java/org/terst/doot/widget/data/WidgetRepository.kt16
-rw-r--r--android/app/src/main/java/org/terst/doot/widget/ui/TaskDetailActivity.kt70
-rw-r--r--cmd/dashboard/main.go1
-rw-r--r--internal/handlers/widget.go31
-rw-r--r--internal/store/native_tasks.go8
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(`