summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-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
-rw-r--r--android/app/src/test/java/org/terst/doot/widget/WidgetRepositoryTest.kt44
5 files changed, 175 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}" }
+ }
+ }
}
diff --git a/android/app/src/test/java/org/terst/doot/widget/WidgetRepositoryTest.kt b/android/app/src/test/java/org/terst/doot/widget/WidgetRepositoryTest.kt
index 31e6642..8712124 100644
--- a/android/app/src/test/java/org/terst/doot/widget/WidgetRepositoryTest.kt
+++ b/android/app/src/test/java/org/terst/doot/widget/WidgetRepositoryTest.kt
@@ -105,4 +105,48 @@ class WidgetRepositoryTest {
assertTrue("expected id=rec-1 in URL, got $url", url.contains("id=rec-1"))
assertTrue("expected source=doot in URL, got $url", url.contains("source=doot"))
}
+
+ @Test
+ fun `fetchProjects returns success with valid JSON`() = runTest {
+ val json = """[{"id":"proj-1","name":"Sailing prep","color":"#3B82F6","created_at":"2026-07-15T00:00:00-10:00"}]"""
+ every { mockClient.newCall(any()) } returns mockCall
+ every { mockCall.execute() } returns responseOf(200, json)
+
+ val repo = WidgetRepository(mockClient, "https://doot.example.com", "token")
+ val result = repo.fetchProjects()
+
+ assertTrue(result.isSuccess)
+ val projects = result.getOrThrow()
+ assertEquals(1, projects.size)
+ assertEquals("Sailing prep", projects[0].name)
+ assertEquals("#3B82F6", projects[0].color)
+ }
+
+ @Test
+ fun `createProject returns success with valid JSON`() = runTest {
+ val json = """{"id":"proj-1","name":"Sailing prep","color":"#3B82F6","created_at":"2026-07-15T00:00:00-10:00"}"""
+ every { mockClient.newCall(any()) } returns mockCall
+ every { mockCall.execute() } returns responseOf(200, json)
+
+ val repo = WidgetRepository(mockClient, "https://doot.example.com", "token")
+ val result = repo.createProject("Sailing prep", "#3B82F6")
+
+ assertTrue(result.isSuccess)
+ assertEquals("Sailing prep", result.getOrThrow().name)
+ }
+
+ @Test
+ fun `fetchLabelColors returns success with valid JSON`() = runTest {
+ val json = """[{"name":"chore","color":"#F59E0B"}]"""
+ every { mockClient.newCall(any()) } returns mockCall
+ every { mockCall.execute() } returns responseOf(200, json)
+
+ val repo = WidgetRepository(mockClient, "https://doot.example.com", "token")
+ val result = repo.fetchLabelColors()
+
+ assertTrue(result.isSuccess)
+ val colors = result.getOrThrow()
+ assertEquals(1, colors.size)
+ assertEquals("chore", colors[0].name)
+ }
}