feat: add configurable widget display period setting (#23)

Instead of removing the time filter entirely, let users choose the
widget display period (1 week, 2 weeks, 4 weeks, or all tasks) from
Settings. Default remains 2 weeks for backward compatibility.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
le king fu 2026-03-12 20:04:35 -04:00
parent dde33acdf2
commit f040ec7902
5 changed files with 91 additions and 12 deletions

View file

@ -2,13 +2,14 @@ import { useState, useEffect, useCallback } from 'react';
import { View, Text, Pressable, useColorScheme, TextInput, ScrollView, Alert, Modal, Platform, Switch, Linking, ActivityIndicator } from 'react-native';
import { KeyboardAvoidingView } from 'react-native-keyboard-controller';
import { useTranslation } from 'react-i18next';
import { Sun, Moon, Smartphone, Plus, Trash2, Pencil, Bell, CalendarDays, Mail, RefreshCw } from 'lucide-react-native';
import { Sun, Moon, Smartphone, Plus, Trash2, Pencil, Bell, CalendarDays, LayoutGrid, Mail, RefreshCw } from 'lucide-react-native';
import Constants from 'expo-constants';
import { colors } from '@/src/theme/colors';
import { useSettingsStore } from '@/src/stores/useSettingsStore';
import { getAllTags, createTag, updateTag, deleteTag } from '@/src/db/repository/tags';
import { initCalendar } from '@/src/services/calendar';
import { syncWidgetData } from '@/src/services/widgetSync';
import i18n from '@/src/i18n';
type ThemeMode = 'light' | 'dark' | 'system';
@ -23,6 +24,7 @@ export default function SettingsScreen() {
notificationsEnabled, setNotificationsEnabled,
reminderOffset, setReminderOffset,
calendarSyncEnabled, setCalendarSyncEnabled,
widgetPeriodWeeks, setWidgetPeriodWeeks,
} = useSettingsStore();
const isDark = (theme === 'system' ? systemScheme : theme) === 'dark';
@ -299,6 +301,56 @@ export default function SettingsScreen() {
</View>
</View>
{/* Widget Section */}
<View className="px-4 pt-6">
<Text
className={`mb-3 text-xs uppercase tracking-wide ${isDark ? 'text-[#A0A0A0]' : 'text-[#6B6B6B]'}`}
style={{ fontFamily: 'Inter_600SemiBold' }}
>
{t('widget.title')}
</Text>
<View className={`overflow-hidden rounded-xl ${isDark ? 'bg-[#2A2A2A]' : 'bg-white'}`}>
<View className="px-4 py-3.5">
<View className="flex-row items-center mb-2">
<LayoutGrid size={20} color={isDark ? '#A0A0A0' : '#6B6B6B'} />
<Text
className={`ml-3 text-sm ${isDark ? 'text-[#A0A0A0]' : 'text-[#6B6B6B]'}`}
style={{ fontFamily: 'Inter_500Medium' }}
>
{t('widget.period')}
</Text>
</View>
<View className="flex-row flex-wrap gap-2">
{[
{ value: 1, label: t('widget.periodWeek', { count: 1 }) },
{ value: 2, label: t('widget.periodWeek', { count: 2 }) },
{ value: 4, label: t('widget.periodWeek', { count: 4 }) },
{ value: 0, label: t('widget.periodAll') },
].map((opt) => {
const isActive = widgetPeriodWeeks === opt.value;
return (
<Pressable
key={opt.value}
onPress={() => {
setWidgetPeriodWeeks(opt.value);
syncWidgetData();
}}
className={`rounded-full px-3 py-1.5 ${isActive ? 'bg-bleu' : isDark ? 'bg-[#3A3A3A]' : 'bg-[#E5E7EB]'}`}
>
<Text
className={`text-sm ${isActive ? 'text-white' : isDark ? 'text-[#F5F5F5]' : 'text-[#1A1A1A]'}`}
style={{ fontFamily: isActive ? 'Inter_600SemiBold' : 'Inter_400Regular' }}
>
{opt.label}
</Text>
</Pressable>
);
})}
</View>
</View>
</View>
</View>
{/* Tags Section */}
<View className="px-4 pt-6">
<View className="mb-3 flex-row items-center justify-between">

View file

@ -138,6 +138,10 @@
"overdue": "Overdue",
"today": "Today",
"tomorrow": "Tomorrow",
"noDate": "No date"
"noDate": "No date",
"period": "Display period",
"periodWeek_one": "{{count}} week",
"periodWeek_other": "{{count}} weeks",
"periodAll": "All"
}
}

View file

@ -138,6 +138,10 @@
"overdue": "En retard",
"today": "Aujourd'hui",
"tomorrow": "Demain",
"noDate": "Sans date"
"noDate": "Sans date",
"period": "Période affichée",
"periodWeek_one": "{{count}} semaine",
"periodWeek_other": "{{count}} semaines",
"periodAll": "Toutes"
}
}

View file

@ -4,7 +4,7 @@ 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, sql } from 'drizzle-orm';
import { startOfDay } from 'date-fns';
import { startOfDay, endOfDay, addWeeks } from 'date-fns';
import { TaskListWidget } from '../widgets/TaskListWidget';
export const WIDGET_DATA_KEY = 'widget:tasks';
@ -35,6 +35,19 @@ export async function syncWidgetData(): Promise<void> {
const now = new Date();
const todayStart = startOfDay(now);
// Read widget period setting from AsyncStorage (0 = all, N = N weeks ahead)
let widgetPeriodWeeks = 2;
try {
const settingsRaw = await AsyncStorage.getItem('simpl-liste-settings');
if (settingsRaw) {
const settings = JSON.parse(settingsRaw);
const stored = settings?.state?.widgetPeriodWeeks;
if (typeof stored === 'number') widgetPeriodWeeks = stored;
}
} catch {
// Default to 2 weeks
}
const selectFields = {
id: tasks.id,
title: tasks.title,
@ -47,18 +60,20 @@ export async function syncWidgetData(): Promise<void> {
subtaskDoneCount: sql<number>`(SELECT COUNT(*) FROM tasks AS sub WHERE sub.parent_id = ${tasks.id} AND sub.completed = 1)`.as('subtask_done_count'),
};
// Fetch all upcoming tasks (today and future)
// Fetch upcoming tasks (filtered by period setting, 0 = all future tasks)
const upcomingConditions = [
eq(tasks.completed, false),
isNull(tasks.parentId),
gte(tasks.dueDate, todayStart),
];
if (widgetPeriodWeeks > 0) {
upcomingConditions.push(lte(tasks.dueDate, endOfDay(addWeeks(now, widgetPeriodWeeks))));
}
const upcomingTasks = await db
.select(selectFields)
.from(tasks)
.leftJoin(lists, eq(tasks.listId, lists.id))
.where(
and(
eq(tasks.completed, false),
isNull(tasks.parentId),
gte(tasks.dueDate, todayStart)
)
)
.where(and(...upcomingConditions))
.orderBy(asc(tasks.dueDate));
// Fetch overdue tasks

View file

@ -10,11 +10,13 @@ interface SettingsState {
notificationsEnabled: boolean;
reminderOffset: number; // hours before due date (0 = at time)
calendarSyncEnabled: boolean;
widgetPeriodWeeks: number; // 0 = all tasks, otherwise number of weeks ahead
setTheme: (theme: ThemeMode) => void;
setLocale: (locale: 'fr' | 'en') => void;
setNotificationsEnabled: (enabled: boolean) => void;
setReminderOffset: (offset: number) => void;
setCalendarSyncEnabled: (enabled: boolean) => void;
setWidgetPeriodWeeks: (weeks: number) => void;
}
export const useSettingsStore = create<SettingsState>()(
@ -25,11 +27,13 @@ export const useSettingsStore = create<SettingsState>()(
notificationsEnabled: true,
reminderOffset: 0,
calendarSyncEnabled: false,
widgetPeriodWeeks: 2,
setTheme: (theme) => set({ theme }),
setLocale: (locale) => set({ locale }),
setNotificationsEnabled: (notificationsEnabled) => set({ notificationsEnabled }),
setReminderOffset: (reminderOffset) => set({ reminderOffset }),
setCalendarSyncEnabled: (calendarSyncEnabled) => set({ calendarSyncEnabled }),
setWidgetPeriodWeeks: (widgetPeriodWeeks) => set({ widgetPeriodWeeks }),
}),
{
name: 'simpl-liste-settings',