Files
honeyDueKMP/composeApp/src/androidMain/kotlin/com/tt/honeyDue/widget/WidgetTaskDto.kt
T
Trey T 9c9e6009c7
Android UI Tests / ui-tests (pull_request) Has been cancelled
feat(widget): per-residence widget configuration (Android, gitea#6)
Mirrors the iOS implementation. Adds a Glance configuration activity
that launches when the user pins a new honeyDue widget tile and again
on "Edit Widget", lets them pick one of their residences (or "All
residences"), and persists the choice per-`appWidgetId`. Each tile's
`provideGlance` resolves its own scope and filters tasks (and stats,
on the large widget) accordingly.

Pieces:

- `WidgetConfigActivity` — Compose `ComponentActivity` hosting the
  residence-picker UI; reads the persisted residences sidecar, reads
  any prior scope for the current `appWidgetId`, writes the new
  selection on Save, and re-renders every widget tile.
- `WidgetDataStore` — new `widget_residences_json` key + a per-instance
  `widget_residence_id_<appWidgetId>` key. `clearAll()` sweeps the
  per-instance keys by prefix so logout doesn't leave dangling state.
- `WidgetDataRepository`:
  * `saveResidences(_)` / `loadResidences()` for the picker.
  * `saveResidenceIdFor(appWidgetId, residenceId)` /
    `loadResidenceIdFor(appWidgetId)` /
    `clearResidenceIdFor(appWidgetId)` for per-tile scope.
  * `loadTasksForResidence(residenceId)` and the
    `appWidgetId`-driven `loadTasksForWidget(appWidgetId)`.
  * `computeStatsFromTasks(tasks)` so the large widget's tiles
    reflect only the scoped task list (instead of the whole cache).
  * Pure `Filter.filterTasksForResidence(_, _)` on the companion
    object — easy to exercise from unit tests.
- `WidgetTaskDto` already carries `residenceId`. New `WidgetResidenceDto`
  added (id + name) — JSON-persisted via the sidecar.
- `WidgetRefreshWorker` / `DefaultWidgetRefreshDataSource` — pull
  `myResidences` alongside tasks/tier on each refresh and write the
  sidecar (best-effort; non-fatal if the call fails).
- `HoneyDue{Small,Medium,Large}Widget.provideGlance` — resolve
  `appWidgetId` via `GlanceAppWidgetManager(context).getAppWidgetId(id)`
  and call `loadTasksForWidget(appWidgetId)`.
- `HoneyDue{Small,Medium,Large}WidgetReceiver.onDeleted` — purge the
  per-instance residence scope key when the tile is removed.
- Manifest: register the configure activity with the
  `APPWIDGET_CONFIGURE` action.
- `honeydue_{small,medium,large}_widget_info.xml` — declare
  `android:configure="com.tt.honeyDue.widget.WidgetConfigActivity"`.

Migration / safety:
- A tile that's never been through the picker has no residence id
  saved → `loadTasksForWidget` returns every task (legacy "All
  residences" behaviour). Existing tiles keep working without the
  user touching anything.
- The picker handles an empty residences list (signed-out / first
  install before background refresh) with an explicit helper message
  pointing at the main app.

Tests: new `WidgetResidenceFilterTest` (commonTest-style under
`androidUnitTest`, 9 cases). All green.

  $ ./gradlew :composeApp:testDebugUnitTest \\
      --tests "com.tt.honeyDue.widget.WidgetResidenceFilterTest"
  BUILD SUCCESSFUL

  $ ./gradlew :composeApp:assembleDebug
  BUILD SUCCESSFUL

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 13:31:46 -05:00

64 lines
1.8 KiB
Kotlin

package com.tt.honeyDue.widget
import kotlinx.serialization.Serializable
/**
* DTO persisted to the widget DataStore as JSON, mirroring iOS
* `WidgetDataManager.swift`'s on-disk task representation.
*
* iOS field map (for reference — keep in sync):
* - id Int (task id)
* - title String
* - priority Int (priority id)
* - dueDate String? ISO-8601 ("yyyy-MM-dd" or full datetime)
* - isOverdue Bool
* - daysUntilDue Int
* - residenceId Int
* - residenceName String
* - categoryIcon String SF-symbol-style identifier
* - completed Bool
*
* Kotlin uses [Long] for ids to accommodate any server-side auto-increment range.
*/
@Serializable
data class WidgetTaskDto(
val id: Long,
val title: String,
val priority: Long,
val dueDate: String?,
val isOverdue: Boolean,
val daysUntilDue: Int,
val residenceId: Long,
val residenceName: String,
val categoryIcon: String,
val completed: Boolean
)
/**
* Lightweight residence identifier persisted to the widget DataStore.
*
* Written by the main app whenever [com.tt.honeyDue.data.DataManager.myResidences]
* updates so the widget configuration activity can offer the current
* residence list (gitea#6 — per-residence widget selection). Mirrors
* iOS' `WidgetDataManager.WidgetResidence` shape.
*/
@Serializable
data class WidgetResidenceDto(
val id: Long,
val name: String
)
/**
* Summary metrics computed from the cached task list.
*
* Windows match iOS `calculateMetrics` semantics:
* - overdueCount tasks with isOverdue == true
* - dueWithin7 tasks with 0 <= daysUntilDue <= 7
* - dueWithin8To30 tasks with 8 <= daysUntilDue <= 30
*/
data class WidgetStats(
val overdueCount: Int,
val dueWithin7: Int,
val dueWithin8To30: Int
)