From f61ce64b5081b608cef5032085b541d73bad842d Mon Sep 17 00:00:00 2001 From: le king fu Date: Sat, 28 Feb 2026 16:47:47 -0500 Subject: [PATCH] feat: show subtask progress in Android widget (#9) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add subtaskCount/subtaskDoneCount to WidgetTask with correlated SQL subqueries - Display subtask indicator (✓ done/total) below task title in widget rows - Update type guard for new fields in headless handler Co-Authored-By: Claude Opus 4.6 --- src/services/widgetSync.ts | 8 +++++++- src/widgets/TaskListWidget.tsx | 13 ++++++++++++- src/widgets/widgetTaskHandler.ts | 4 +++- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/services/widgetSync.ts b/src/services/widgetSync.ts index 8b058ae..14d6426 100644 --- a/src/services/widgetSync.ts +++ b/src/services/widgetSync.ts @@ -3,7 +3,7 @@ import { requestWidgetUpdate } from 'react-native-android-widget'; import AsyncStorage from '@react-native-async-storage/async-storage'; import { db } from '../db/client'; import { tasks, lists } from '../db/schema'; -import { eq, and, isNull, gte, lte, lt, asc } from 'drizzle-orm'; +import { eq, and, isNull, gte, lte, lt, asc, sql } from 'drizzle-orm'; import { startOfDay, endOfDay, addWeeks } from 'date-fns'; import { TaskListWidget } from '../widgets/TaskListWidget'; @@ -17,6 +17,8 @@ export interface WidgetTask { dueDate: string | null; completed: boolean; listColor: string | null; + subtaskCount: number; + subtaskDoneCount: number; } export async function syncWidgetData(): Promise { @@ -35,6 +37,8 @@ export async function syncWidgetData(): Promise { completed: tasks.completed, position: tasks.position, listColor: lists.color, + subtaskCount: sql`(SELECT COUNT(*) FROM tasks AS sub WHERE sub.parent_id = ${tasks.id})`.as('subtask_count'), + subtaskDoneCount: sql`(SELECT COUNT(*) FROM tasks AS sub WHERE sub.parent_id = ${tasks.id} AND sub.completed = 1)`.as('subtask_done_count'), }; // Fetch tasks with due date in the next 2 weeks @@ -87,6 +91,8 @@ export async function syncWidgetData(): Promise { dueDate: t.dueDate ? new Date(t.dueDate).toISOString() : null, completed: t.completed, listColor: t.listColor, + subtaskCount: t.subtaskCount ?? 0, + subtaskDoneCount: t.subtaskDoneCount ?? 0, }); // Combine: overdue first, then upcoming, then no date diff --git a/src/widgets/TaskListWidget.tsx b/src/widgets/TaskListWidget.tsx index 0d9a0a6..47b41bf 100644 --- a/src/widgets/TaskListWidget.tsx +++ b/src/widgets/TaskListWidget.tsx @@ -148,7 +148,7 @@ function TaskItemRow({ task, isDark }: { task: WidgetTask; isDark: boolean }) { }} /> ) : null} - + + {(task.subtaskCount ?? 0) > 0 ? ( + + ) : null} diff --git a/src/widgets/widgetTaskHandler.ts b/src/widgets/widgetTaskHandler.ts index 3686a28..acb6e11 100644 --- a/src/widgets/widgetTaskHandler.ts +++ b/src/widgets/widgetTaskHandler.ts @@ -13,7 +13,9 @@ function isWidgetTask(item: unknown): item is WidgetTask { typeof obj.priority === 'number' && typeof obj.completed === 'boolean' && (obj.dueDate === null || typeof obj.dueDate === 'string') && - (obj.listColor === null || obj.listColor === undefined || typeof obj.listColor === 'string') + (obj.listColor === null || obj.listColor === undefined || typeof obj.listColor === 'string') && + (obj.subtaskCount === undefined || typeof obj.subtaskCount === 'number') && + (obj.subtaskDoneCount === undefined || typeof obj.subtaskDoneCount === 'number') ); }