feat: show subtask progress in Android widget (#9)

- 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 <noreply@anthropic.com>
This commit is contained in:
le king fu 2026-02-28 16:47:47 -05:00
parent 72eafbd9d9
commit f61ce64b50
3 changed files with 22 additions and 3 deletions

View file

@ -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<void> {
@ -35,6 +37,8 @@ export async function syncWidgetData(): Promise<void> {
completed: tasks.completed,
position: tasks.position,
listColor: lists.color,
subtaskCount: sql<number>`(SELECT COUNT(*) FROM tasks AS sub WHERE sub.parent_id = ${tasks.id})`.as('subtask_count'),
subtaskDoneCount: sql<number>`(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<void> {
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

View file

@ -148,7 +148,7 @@ function TaskItemRow({ task, isDark }: { task: WidgetTask; isDark: boolean }) {
}}
/>
) : null}
<FlexWidget style={{ flex: 1 }}>
<FlexWidget style={{ flex: 1, flexDirection: 'column' }}>
<TextWidget
text={task.title}
maxLines={1}
@ -159,6 +159,17 @@ function TaskItemRow({ task, isDark }: { task: WidgetTask; isDark: boolean }) {
color: c.text,
}}
/>
{(task.subtaskCount ?? 0) > 0 ? (
<TextWidget
text={`${task.subtaskDoneCount ?? 0}/${task.subtaskCount}`}
style={{
fontSize: 11,
fontFamily: FONT_REGULAR,
color: task.subtaskDoneCount === task.subtaskCount ? TODAY_COLOR : c.textSecondary,
marginTop: 1,
}}
/>
) : null}
</FlexWidget>
</FlexWidget>

View file

@ -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')
);
}