summaryrefslogtreecommitdiff
path: root/android/app/src/main
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-07-15 18:36:00 +0000
committerPeter Stone <thepeterstone@gmail.com>2026-07-16 02:54:17 +0000
commit0aa5bd0de79df82958a7314a7db6e91e892d28b7 (patch)
treee3829836614846d2faa436cdbfd6802e9676472b /android/app/src/main
parentc1ba9994367dd077ba2422d31a658a0c62e2520b (diff)
feat(widget): add Android data layer for projects and label colors
Diffstat (limited to 'android/app/src/main')
-rw-r--r--android/app/src/main/java/org/terst/doot/widget/data/Project.kt17
-rw-r--r--android/app/src/main/java/org/terst/doot/widget/data/TaskDetail.kt4
-rw-r--r--android/app/src/main/java/org/terst/doot/widget/data/WidgetItem.kt3
-rw-r--r--android/app/src/main/java/org/terst/doot/widget/data/WidgetRepository.kt109
4 files changed, 131 insertions, 2 deletions
diff --git a/android/app/src/main/java/org/terst/doot/widget/data/Project.kt b/android/app/src/main/java/org/terst/doot/widget/data/Project.kt
new file mode 100644
index 0000000..43ac63b
--- /dev/null
+++ b/android/app/src/main/java/org/terst/doot/widget/data/Project.kt
@@ -0,0 +1,17 @@
+package org.terst.doot.widget.data
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class Project(
+ val id: String,
+ val name: String,
+ val color: String,
+ @kotlinx.serialization.SerialName("created_at") val createdAt: String
+)
+
+@Serializable
+data class LabelColor(
+ val name: String,
+ val color: String
+)
diff --git a/android/app/src/main/java/org/terst/doot/widget/data/TaskDetail.kt b/android/app/src/main/java/org/terst/doot/widget/data/TaskDetail.kt
index 3e1d9f8..515678e 100644
--- a/android/app/src/main/java/org/terst/doot/widget/data/TaskDetail.kt
+++ b/android/app/src/main/java/org/terst/doot/widget/data/TaskDetail.kt
@@ -11,7 +11,9 @@ data class TaskDetailResponse(
@SerialName("due_date") val dueDate: String? = null,
val completed: Boolean = false,
val recurrence: TaskRecurrence? = null,
- @SerialName("next_date") val nextDate: String? = null
+ @SerialName("next_date") val nextDate: String? = null,
+ val project: Project? = null,
+ val labels: List<String> = emptyList()
)
@Serializable
diff --git a/android/app/src/main/java/org/terst/doot/widget/data/WidgetItem.kt b/android/app/src/main/java/org/terst/doot/widget/data/WidgetItem.kt
index fee15e1..92598ba 100644
--- a/android/app/src/main/java/org/terst/doot/widget/data/WidgetItem.kt
+++ b/android/app/src/main/java/org/terst/doot/widget/data/WidgetItem.kt
@@ -16,7 +16,8 @@ data class WidgetItem(
@SerialName("due_date") val dueDate: String? = null,
val url: String = "",
val completable: Boolean = false,
- @SerialName("recurring_event_id") val recurringEventId: String? = null
+ @SerialName("recurring_event_id") val recurringEventId: String? = null,
+ @SerialName("project_color") val projectColor: String? = null
)
@Serializable
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 8b27dac..16c76f2 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
@@ -4,6 +4,7 @@ import android.content.Context
import androidx.datastore.preferences.core.edit
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
+import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
@@ -218,4 +219,112 @@ class WidgetRepository(
check(response.isSuccessful) { "HTTP ${response.code}" }
}
}
+
+ /** GETs the non-archived project list from /api/widget/projects. */
+ suspend fun fetchProjects(): Result<List<Project>> =
+ withContext(Dispatchers.IO) {
+ val request = Request.Builder()
+ .url("$serverUrl/api/widget/projects")
+ .header("Authorization", "Bearer $token")
+ .build()
+ runCatching {
+ val response = client.newCall(request).execute()
+ check(response.isSuccessful) { "HTTP ${response.code}" }
+ val body = checkNotNull(response.body?.string()) { "Empty body" }
+ json.decodeFromString<List<Project>>(body)
+ }
+ }
+
+ @Serializable
+ private data class ProjectCreateRequest(val name: String, val color: String)
+
+ /** POSTs a new project to /api/widget/projects. */
+ suspend fun createProject(name: String, color: String): Result<Project> =
+ withContext(Dispatchers.IO) {
+ val body = json.encodeToString(ProjectCreateRequest(name, color))
+ .toRequestBody("application/json".toMediaType())
+ val request = Request.Builder()
+ .url("$serverUrl/api/widget/projects")
+ .header("Authorization", "Bearer $token")
+ .post(body)
+ .build()
+ runCatching {
+ val response = client.newCall(request).execute()
+ check(response.isSuccessful) { "HTTP ${response.code}" }
+ val responseBody = checkNotNull(response.body?.string()) { "Empty body" }
+ json.decodeFromString<Project>(responseBody)
+ }
+ }
+
+ @Serializable
+ private data class TaskProjectRequest(val id: String, @SerialName("project_id") val projectId: String)
+
+ /** POSTs a project assignment to /api/widget/task/project. projectId="" clears it. */
+ suspend fun setTaskProject(id: String, projectId: String): Result<Unit> =
+ withContext(Dispatchers.IO) {
+ val body = json.encodeToString(TaskProjectRequest(id, projectId))
+ .toRequestBody("application/json".toMediaType())
+ val request = Request.Builder()
+ .url("$serverUrl/api/widget/task/project")
+ .header("Authorization", "Bearer $token")
+ .post(body)
+ .build()
+ runCatching {
+ val response = client.newCall(request).execute()
+ check(response.isSuccessful) { "HTTP ${response.code}" }
+ }
+ }
+
+ @Serializable
+ private data class TaskLabelsRequest(val id: String, val labels: List<String>)
+
+ /** POSTs a full label-set replacement to /api/widget/task/labels. */
+ suspend fun setTaskLabels(id: String, labels: List<String>): Result<Unit> =
+ withContext(Dispatchers.IO) {
+ val body = json.encodeToString(TaskLabelsRequest(id, labels))
+ .toRequestBody("application/json".toMediaType())
+ val request = Request.Builder()
+ .url("$serverUrl/api/widget/task/labels")
+ .header("Authorization", "Bearer $token")
+ .post(body)
+ .build()
+ runCatching {
+ val response = client.newCall(request).execute()
+ check(response.isSuccessful) { "HTTP ${response.code}" }
+ }
+ }
+
+ /** GETs every colored label from /api/widget/labels. */
+ suspend fun fetchLabelColors(): Result<List<LabelColor>> =
+ withContext(Dispatchers.IO) {
+ val request = Request.Builder()
+ .url("$serverUrl/api/widget/labels")
+ .header("Authorization", "Bearer $token")
+ .build()
+ runCatching {
+ val response = client.newCall(request).execute()
+ check(response.isSuccessful) { "HTTP ${response.code}" }
+ val body = checkNotNull(response.body?.string()) { "Empty body" }
+ json.decodeFromString<List<LabelColor>>(body)
+ }
+ }
+
+ @Serializable
+ private data class LabelColorSetRequest(val name: String, val color: String)
+
+ /** POSTs a label color assignment to /api/widget/labels/color. */
+ suspend fun setLabelColor(name: String, color: String): Result<Unit> =
+ withContext(Dispatchers.IO) {
+ val body = json.encodeToString(LabelColorSetRequest(name, color))
+ .toRequestBody("application/json".toMediaType())
+ val request = Request.Builder()
+ .url("$serverUrl/api/widget/labels/color")
+ .header("Authorization", "Bearer $token")
+ .post(body)
+ .build()
+ runCatching {
+ val response = client.newCall(request).execute()
+ check(response.isSuccessful) { "HTTP ${response.code}" }
+ }
+ }
}