feat: change medium widget to 2x4, show list color indicator

Change medium widget from 4x2 (wide) to 2x4 (tall) with horizontal
resize support. Add a colored vertical bar before each task showing the
list color for quick visual identification.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
le king fu 2026-02-21 13:42:44 -05:00
parent 0ebc340f37
commit e6867d84b3
4 changed files with 95 additions and 67 deletions

View file

@ -31,49 +31,59 @@
"expo-font",
"expo-localization",
"@react-native-community/datetimepicker",
["react-native-android-widget", {
"fonts": [
"./assets/fonts/Inter_400Regular.ttf",
"./assets/fonts/Inter_600SemiBold.ttf"
],
"widgets": [
{
"name": "SimplListeSmall",
"label": "Simpl-Liste",
"description": "Aperçu rapide de vos tâches",
"minWidth": "110dp",
"minHeight": "110dp",
"targetCellWidth": 2,
"targetCellHeight": 2,
"updatePeriodMillis": 1800000
},
{
"name": "SimplListeMedium",
"label": "Simpl-Liste",
"description": "Vos prochaines tâches",
"minWidth": "250dp",
"minHeight": "110dp",
"targetCellWidth": 4,
"targetCellHeight": 2,
"resizeMode": "vertical",
"updatePeriodMillis": 1800000
},
{
"name": "SimplListeLarge",
"label": "Simpl-Liste (grand)",
"description": "Vue détaillée de vos tâches",
"minWidth": "250dp",
"minHeight": "250dp",
"targetCellWidth": 4,
"targetCellHeight": 4,
"resizeMode": "horizontal|vertical",
"updatePeriodMillis": 1800000
}
]
}]
[
"react-native-android-widget",
{
"fonts": [
"./assets/fonts/Inter_400Regular.ttf",
"./assets/fonts/Inter_600SemiBold.ttf"
],
"widgets": [
{
"name": "SimplListeSmall",
"label": "Simpl-Liste",
"description": "Aperçu rapide de vos tâches",
"minWidth": "110dp",
"minHeight": "110dp",
"targetCellWidth": 2,
"targetCellHeight": 2,
"updatePeriodMillis": 1800000
},
{
"name": "SimplListeMedium",
"label": "Simpl-Liste",
"description": "Vos prochaines tâches",
"minWidth": "110dp",
"minHeight": "250dp",
"targetCellWidth": 2,
"targetCellHeight": 4,
"resizeMode": "horizontal",
"updatePeriodMillis": 1800000
},
{
"name": "SimplListeLarge",
"label": "Simpl-Liste (grand)",
"description": "Vue détaillée de vos tâches",
"minWidth": "250dp",
"minHeight": "250dp",
"targetCellWidth": 4,
"targetCellHeight": 4,
"resizeMode": "horizontal|vertical",
"updatePeriodMillis": 1800000
}
]
}
]
],
"experiments": {
"typedRoutes": true
}
},
"extra": {
"router": {},
"eas": {
"projectId": "eea9da71-e467-49ba-9b59-2febc86a4d1b"
}
},
"owner": "lacompagniemaximus"
}
}

View file

@ -2,7 +2,7 @@ import { Platform } from 'react-native';
import { requestWidgetUpdate } from 'react-native-android-widget';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { db } from '../db/client';
import { tasks } from '../db/schema';
import { tasks, lists } from '../db/schema';
import { eq, and, isNull, gte, lte, lt, asc } from 'drizzle-orm';
import { startOfDay, endOfDay, addWeeks } from 'date-fns';
import { TaskListWidget } from '../widgets/TaskListWidget';
@ -15,6 +15,7 @@ export interface WidgetTask {
priority: number;
dueDate: string | null;
completed: boolean;
listColor: string | null;
}
export async function syncWidgetData(): Promise<void> {
@ -25,10 +26,21 @@ export async function syncWidgetData(): Promise<void> {
const todayStart = startOfDay(now);
const twoWeeksEnd = endOfDay(addWeeks(now, 2));
const selectFields = {
id: tasks.id,
title: tasks.title,
priority: tasks.priority,
dueDate: tasks.dueDate,
completed: tasks.completed,
position: tasks.position,
listColor: lists.color,
};
// Fetch tasks with due date in the next 2 weeks
const upcomingTasks = await db
.select()
.select(selectFields)
.from(tasks)
.leftJoin(lists, eq(tasks.listId, lists.id))
.where(
and(
eq(tasks.completed, false),
@ -41,8 +53,9 @@ export async function syncWidgetData(): Promise<void> {
// Fetch overdue tasks
const overdueTasks = await db
.select()
.select(selectFields)
.from(tasks)
.leftJoin(lists, eq(tasks.listId, lists.id))
.where(
and(
eq(tasks.completed, false),
@ -54,8 +67,9 @@ export async function syncWidgetData(): Promise<void> {
// Fetch tasks without a due date
const noDateTasks = await db
.select()
.select(selectFields)
.from(tasks)
.leftJoin(lists, eq(tasks.listId, lists.id))
.where(
and(
eq(tasks.completed, false),
@ -65,29 +79,20 @@ export async function syncWidgetData(): Promise<void> {
)
.orderBy(asc(tasks.position));
const toWidgetTask = (t: typeof upcomingTasks[number]): WidgetTask => ({
id: t.id,
title: t.title,
priority: t.priority,
dueDate: t.dueDate ? new Date(t.dueDate).toISOString() : null,
completed: t.completed,
listColor: t.listColor,
});
// Combine: overdue first, then upcoming, then no date
const allTasks: WidgetTask[] = [
...overdueTasks.map((t) => ({
id: t.id,
title: t.title,
priority: t.priority,
dueDate: t.dueDate ? new Date(t.dueDate).toISOString() : null,
completed: t.completed,
})),
...upcomingTasks.map((t) => ({
id: t.id,
title: t.title,
priority: t.priority,
dueDate: t.dueDate ? new Date(t.dueDate).toISOString() : null,
completed: t.completed,
})),
...noDateTasks.map((t) => ({
id: t.id,
title: t.title,
priority: t.priority,
dueDate: null,
completed: t.completed,
})),
...overdueTasks.map(toWidgetTask),
...upcomingTasks.map(toWidgetTask),
...noDateTasks.map(toWidgetTask),
];
await AsyncStorage.setItem(WIDGET_DATA_KEY, JSON.stringify(allTasks));

View file

@ -24,6 +24,7 @@ const BORDER_COLOR = '#E5E7EB' as const;
const OVERDUE_COLOR = '#C17767' as const;
const TODAY_COLOR = '#4A90A4' as const;
const CHECKBOX_UNCHECKED = '#D1D5DB' as const;
const DEFAULT_LIST_COLOR = '#4A90A4' as const;
const PRIORITY_COLORS = {
3: '#C17767' as const,
@ -82,6 +83,17 @@ function TaskItemRow({ task }: { task: WidgetTask }) {
clickAction="OPEN_URI"
clickActionData={{ uri: `simplliste:///task/${task.id}` }}
>
{/* List color indicator */}
<FlexWidget
style={{
width: 4,
height: 28,
borderRadius: 2,
backgroundColor: (task.listColor ?? DEFAULT_LIST_COLOR) as ColorProp,
marginRight: 8,
}}
/>
{/* Checkbox */}
<FlexWidget
style={{

View file

@ -12,7 +12,8 @@ function isWidgetTask(item: unknown): item is WidgetTask {
typeof obj.title === 'string' &&
typeof obj.priority === 'number' &&
typeof obj.completed === 'boolean' &&
(obj.dueDate === null || typeof obj.dueDate === 'string')
(obj.dueDate === null || typeof obj.dueDate === 'string') &&
(obj.listColor === null || obj.listColor === undefined || typeof obj.listColor === 'string')
);
}