Compare commits

..

No commits in common. "master" and "v1.2.2" have entirely different histories.

18 changed files with 602 additions and 279 deletions

View file

@ -1,7 +0,0 @@
---
paths: ["**/*.tsx", "**/*.ts"]
---
Toute chaine visible par l'utilisateur doit passer par i18n (react-i18next).
Fichiers : `src/i18n/fr.json` et `src/i18n/en.json`. Francais par defaut.
Jamais de texte en dur dans les composants React.
Toujours ajouter la cle dans les DEUX langues.

View file

@ -1,6 +0,0 @@
---
paths: ["**/migrations/**", "**/*.sql", "**/schema.ts"]
---
Ne JAMAIS modifier une migration SQL existante. Toujours creer une nouvelle migration.
Apres `npx drizzle-kit generate`, mettre a jour `src/db/migrations/migrations.js` si necessaire.
Les migrations sont auto-appliquees au demarrage via `useMigrations()`.

3
.gitignore vendored
View file

@ -31,8 +31,7 @@ yarn-error.*
*.pem
# local env files
.env
.env.*
.env*.local
# typescript
*.tsbuildinfo

View file

@ -2,7 +2,7 @@
"expo": {
"name": "Simpl-Liste",
"slug": "simpl-liste",
"version": "1.3.0",
"version": "1.2.2",
"orientation": "portrait",
"icon": "./assets/images/icon.png",
"scheme": "simplliste",
@ -24,7 +24,7 @@
"backgroundColor": "#FFF8F0"
},
"edgeToEdgeEnabled": true,
"versionCode": 6
"versionCode": 2
},
"plugins": [
"expo-router",

View file

@ -1,9 +1,8 @@
import { useEffect, useState, useCallback, useRef } from 'react';
import {
View, Text, Pressable, useColorScheme, TextInput, Alert,
Modal, Platform, ScrollView,
Modal, KeyboardAvoidingView, Platform, ScrollView,
} from 'react-native';
import { KeyboardAvoidingView } from 'react-native-keyboard-controller';
import { useRouter } from 'expo-router';
import {
Plus, ChevronRight, Check, GripVertical,
@ -250,8 +249,8 @@ export default function ListsScreen() {
{/* Create/Edit Modal */}
<Modal visible={showModal} transparent animationType="fade">
<KeyboardAvoidingView
behavior="padding"
style={{ flex: 1 }}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
className="flex-1"
>
<Pressable onPress={() => setShowModal(false)} className="flex-1 justify-center bg-black/40 px-6">
<Pressable

View file

@ -1,15 +1,13 @@
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 { View, Text, Pressable, useColorScheme, TextInput, ScrollView, Alert, Modal, KeyboardAvoidingView, Platform, Switch, Linking, ActivityIndicator } from 'react-native';
import { useTranslation } from 'react-i18next';
import { Sun, Moon, Smartphone, Plus, Trash2, Pencil, Bell, CalendarDays, LayoutGrid, Mail, RefreshCw } from 'lucide-react-native';
import { Sun, Moon, Smartphone, Plus, Trash2, Pencil, Bell, CalendarDays, 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';
@ -24,7 +22,6 @@ export default function SettingsScreen() {
notificationsEnabled, setNotificationsEnabled,
reminderOffset, setReminderOffset,
calendarSyncEnabled, setCalendarSyncEnabled,
widgetPeriodWeeks, setWidgetPeriodWeeks,
} = useSettingsStore();
const isDark = (theme === 'system' ? systemScheme : theme) === 'dark';
@ -301,56 +298,6 @@ 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">
@ -405,8 +352,8 @@ export default function SettingsScreen() {
{/* Tag Create/Edit Modal */}
<Modal visible={showTagModal} transparent animationType="fade">
<KeyboardAvoidingView
behavior="padding"
style={{ flex: 1 }}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
className="flex-1"
>
<Pressable onPress={() => setShowTagModal(false)} className="flex-1 justify-center bg-black/40 px-6">
<Pressable

View file

@ -6,7 +6,6 @@ import { useFonts, Inter_400Regular, Inter_500Medium, Inter_600SemiBold, Inter_7
import * as SplashScreen from 'expo-splash-screen';
import { useMigrations } from 'drizzle-orm/expo-sqlite/migrator';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { KeyboardProvider } from 'react-native-keyboard-controller';
import { db } from '@/src/db/client';
import migrations from '@/src/db/migrations/migrations';
@ -80,25 +79,23 @@ export default function RootLayout() {
return (
<GestureHandlerRootView style={{ flex: 1 }}>
<KeyboardProvider>
<ThemeProvider value={effectiveScheme === 'dark' ? SimplDarkTheme : SimplLightTheme}>
<Stack>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen
name="task/new"
options={{ presentation: 'modal', headerShown: false }}
/>
<Stack.Screen
name="task/[id]"
options={{ headerShown: false }}
/>
<Stack.Screen
name="list/[id]"
options={{ headerShown: false }}
/>
</Stack>
</ThemeProvider>
</KeyboardProvider>
<ThemeProvider value={effectiveScheme === 'dark' ? SimplDarkTheme : SimplLightTheme}>
<Stack>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen
name="task/new"
options={{ presentation: 'modal', headerShown: false }}
/>
<Stack.Screen
name="task/[id]"
options={{ headerShown: false }}
/>
<Stack.Screen
name="list/[id]"
options={{ headerShown: false }}
/>
</Stack>
</ThemeProvider>
</GestureHandlerRootView>
);
}

View file

@ -1,14 +1,15 @@
import { useEffect, useState } from 'react';
import { useEffect, useState, useRef } from 'react';
import {
View,
Text,
TextInput,
Pressable,
ScrollView,
useColorScheme,
Alert,
Platform,
Keyboard,
} from 'react-native';
import { KeyboardAwareScrollView } from 'react-native-keyboard-controller';
import { useRouter, useLocalSearchParams } from 'expo-router';
import {
ArrowLeft, Plus, Trash2, Calendar, X, Repeat, Download,
@ -25,7 +26,6 @@ import { colors } from '@/src/theme/colors';
import { useSettingsStore } from '@/src/stores/useSettingsStore';
import { isValidUUID } from '@/src/lib/validation';
import { getPriorityOptions } from '@/src/lib/priority';
import { goBack } from '@/src/lib/navigation';
import { RECURRENCE_OPTIONS } from '@/src/lib/recurrence';
import {
getTaskById,
@ -81,6 +81,22 @@ export default function TaskDetailScreen() {
const [lists, setLists] = useState<{ id: string; name: string; color: string | null; icon: string | null; isInbox: boolean }[]>([]);
const [selectedListId, setSelectedListId] = useState<string>('');
const [saving, setSaving] = useState(false);
const scrollRef = useRef<ScrollView>(null);
const subtaskFocused = useRef(false);
const [keyboardHeight, setKeyboardHeight] = useState(0);
useEffect(() => {
const showSub = Keyboard.addListener('keyboardDidShow', (e) => {
setKeyboardHeight(e.endCoordinates.height);
if (subtaskFocused.current) {
setTimeout(() => scrollRef.current?.scrollToEnd({ animated: true }), 100);
}
});
const hideSub = Keyboard.addListener('keyboardDidHide', () => {
setKeyboardHeight(0);
});
return () => { showSub.remove(); hideSub.remove(); };
}, []);
useEffect(() => {
if (!isValidUUID(id)) {
@ -127,9 +143,8 @@ export default function TaskDetailScreen() {
listId: selectedListId,
});
await setTagsForTask(task.id, selectedTagIds);
goBack(router);
router.back();
} catch {
// Save failed — stay on screen so user can retry
setSaving(false);
}
};
@ -143,7 +158,7 @@ export default function TaskDetailScreen() {
onPress: async () => {
await deleteTask(id!);
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
goBack(router);
router.back();
},
},
]);
@ -187,7 +202,7 @@ export default function TaskDetailScreen() {
<View
className={`flex-row items-center justify-between border-b px-4 pb-3 pt-14 ${isDark ? 'border-[#3A3A3A]' : 'border-[#E5E7EB]'}`}
>
<Pressable onPress={() => goBack(router)} className="p-2.5" hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}>
<Pressable onPress={() => router.back()} className="p-1">
<ArrowLeft size={24} color={isDark ? '#F5F5F5' : '#1A1A1A'} />
</Pressable>
<View className="flex-row items-center">
@ -197,22 +212,21 @@ export default function TaskDetailScreen() {
[{ id: id!, title, notes: notes || null, dueDate, priority, completed: task.completed, recurrence }],
title
)}
className="mr-3 p-2.5"
hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
className="mr-3 p-1"
>
<Download size={20} color={isDark ? '#A0A0A0' : '#6B6B6B'} />
</Pressable>
)}
<Pressable onPress={handleDelete} className="mr-3 p-2.5" hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}>
<Pressable onPress={handleDelete} className="mr-3 p-1">
<Trash2 size={20} color={colors.terracotta.DEFAULT} />
</Pressable>
<Pressable onPress={handleSave} disabled={saving} className={`rounded-lg bg-bleu px-4 py-2 ${saving ? 'opacity-50' : ''}`}>
<Pressable onPress={handleSave} disabled={saving} className={`rounded-lg bg-bleu px-4 py-1.5 ${saving ? 'opacity-50' : ''}`}>
<Text className="text-sm text-white" style={{ fontFamily: 'Inter_600SemiBold' }}>{t('common.save')}</Text>
</Pressable>
</View>
</View>
<KeyboardAwareScrollView className="flex-1 px-4 pt-4" keyboardShouldPersistTaps="handled" bottomOffset={20}>
<ScrollView ref={scrollRef} className="flex-1 px-4 pt-4" keyboardShouldPersistTaps="handled">
{/* Title */}
<TextInput
value={title}
@ -401,6 +415,8 @@ export default function TaskDetailScreen() {
value={newSubtask}
onChangeText={setNewSubtask}
onSubmitEditing={handleAddSubtask}
onFocus={() => { subtaskFocused.current = true; }}
onBlur={() => { subtaskFocused.current = false; }}
placeholder={t('task.addSubtask')}
placeholderTextColor={isDark ? '#A0A0A0' : '#6B6B6B'}
className={`ml-2 flex-1 text-base ${isDark ? 'text-[#F5F5F5]' : 'text-[#1A1A1A]'}`}
@ -408,8 +424,8 @@ export default function TaskDetailScreen() {
/>
</View>
<View style={{ height: 32 }} />
</KeyboardAwareScrollView>
<View style={{ height: keyboardHeight || 32 }} />
</ScrollView>
</View>
);
}

View file

@ -1,13 +1,14 @@
import { useState, useEffect } from 'react';
import { useState, useEffect, useRef } from 'react';
import {
View,
Text,
TextInput,
Pressable,
ScrollView,
useColorScheme,
Platform,
Keyboard,
} from 'react-native';
import { KeyboardAwareScrollView } from 'react-native-keyboard-controller';
import { useRouter, useLocalSearchParams } from 'expo-router';
import {
X, Calendar, Repeat, Plus,
@ -27,7 +28,6 @@ import { getInboxId, getAllLists } from '@/src/db/repository/lists';
import { getAllTags, setTagsForTask } from '@/src/db/repository/tags';
import { getPriorityOptions } from '@/src/lib/priority';
import { RECURRENCE_OPTIONS } from '@/src/lib/recurrence';
import { goBack } from '@/src/lib/navigation';
import TagChip from '@/src/components/task/TagChip';
const ICON_MAP: Record<string, LucideIcon> = {
@ -59,6 +59,22 @@ export default function NewTaskScreen() {
const [saving, setSaving] = useState(false);
const [pendingSubtasks, setPendingSubtasks] = useState<string[]>([]);
const [newSubtask, setNewSubtask] = useState('');
const scrollRef = useRef<ScrollView>(null);
const subtaskFocused = useRef(false);
const [keyboardHeight, setKeyboardHeight] = useState(0);
useEffect(() => {
const showSub = Keyboard.addListener('keyboardDidShow', (e) => {
setKeyboardHeight(e.endCoordinates.height);
if (subtaskFocused.current) {
setTimeout(() => scrollRef.current?.scrollToEnd({ animated: true }), 100);
}
});
const hideSub = Keyboard.addListener('keyboardDidHide', () => {
setKeyboardHeight(0);
});
return () => { showSub.remove(); hideSub.remove(); };
}, []);
useEffect(() => {
getAllLists().then(setLists);
@ -84,7 +100,7 @@ export default function NewTaskScreen() {
for (const sub of pendingSubtasks) {
await createTask({ title: sub, listId: selectedListId, parentId: taskId });
}
goBack(router);
router.back();
} catch {
// FK constraint or other DB error — fallback to inbox
setSaving(false);
@ -121,7 +137,7 @@ export default function NewTaskScreen() {
isDark ? 'border-[#3A3A3A]' : 'border-[#E5E7EB]'
}`}
>
<Pressable onPress={() => goBack(router)} className="p-2.5" hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}>
<Pressable onPress={() => router.back()} className="p-1">
<X size={24} color={isDark ? '#F5F5F5' : '#1A1A1A'} />
</Pressable>
<Text
@ -130,14 +146,14 @@ export default function NewTaskScreen() {
>
{t('task.newTask')}
</Text>
<Pressable onPress={handleSave} disabled={saving} className={`rounded-lg bg-bleu px-4 py-2 ${saving ? 'opacity-50' : ''}`}>
<Pressable onPress={handleSave} disabled={saving} className={`rounded-lg bg-bleu px-4 py-1.5 ${saving ? 'opacity-50' : ''}`}>
<Text className="text-sm text-white" style={{ fontFamily: 'Inter_600SemiBold' }}>
{t('common.save')}
</Text>
</Pressable>
</View>
<KeyboardAwareScrollView className="flex-1 px-4 pt-4" keyboardShouldPersistTaps="handled" bottomOffset={20}>
<ScrollView ref={scrollRef} className="flex-1 px-4 pt-4" keyboardShouldPersistTaps="handled">
{/* Title */}
<TextInput
autoFocus
@ -369,6 +385,8 @@ export default function NewTaskScreen() {
value={newSubtask}
onChangeText={setNewSubtask}
onSubmitEditing={handleAddPendingSubtask}
onFocus={() => { subtaskFocused.current = true; }}
onBlur={() => { subtaskFocused.current = false; }}
placeholder={t('task.addSubtask')}
placeholderTextColor={isDark ? '#A0A0A0' : '#6B6B6B'}
className={`ml-2 flex-1 text-base ${isDark ? 'text-[#F5F5F5]' : 'text-[#1A1A1A]'}`}
@ -376,8 +394,8 @@ export default function NewTaskScreen() {
/>
</View>
<View style={{ height: 32 }} />
</KeyboardAwareScrollView>
<View style={{ height: keyboardHeight || 32 }} />
</ScrollView>
</View>
);
}

475
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "simpl-liste",
"version": "1.3.0",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "simpl-liste",
"version": "1.3.0",
"version": "1.0.0",
"dependencies": {
"@expo-google-fonts/inter": "^0.4.2",
"@expo/ngrok": "^4.1.3",
@ -43,7 +43,6 @@
"react-native-android-widget": "^0.20.1",
"react-native-draggable-flatlist": "^4.0.3",
"react-native-gesture-handler": "~2.28.0",
"react-native-keyboard-controller": "1.18.5",
"react-native-reanimated": "~4.1.1",
"react-native-safe-area-context": "~5.6.0",
"react-native-screens": "~4.16.0",
@ -1594,6 +1593,418 @@
"source-map-support": "^0.5.21"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz",
"integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz",
"integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz",
"integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-arm64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz",
"integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz",
"integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-arm64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz",
"integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz",
"integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz",
"integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz",
"integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ia32": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz",
"integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-loong64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz",
"integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-mips64el": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz",
"integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ppc64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz",
"integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-riscv64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz",
"integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-s390x": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz",
"integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz",
"integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/netbsd-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz",
"integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/openbsd-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz",
"integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/sunos-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz",
"integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-arm64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz",
"integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-ia32": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz",
"integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz",
"integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/esbuild": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz",
"integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=12"
},
"optionalDependencies": {
"@esbuild/android-arm": "0.18.20",
"@esbuild/android-arm64": "0.18.20",
"@esbuild/android-x64": "0.18.20",
"@esbuild/darwin-arm64": "0.18.20",
"@esbuild/darwin-x64": "0.18.20",
"@esbuild/freebsd-arm64": "0.18.20",
"@esbuild/freebsd-x64": "0.18.20",
"@esbuild/linux-arm": "0.18.20",
"@esbuild/linux-arm64": "0.18.20",
"@esbuild/linux-ia32": "0.18.20",
"@esbuild/linux-loong64": "0.18.20",
"@esbuild/linux-mips64el": "0.18.20",
"@esbuild/linux-ppc64": "0.18.20",
"@esbuild/linux-riscv64": "0.18.20",
"@esbuild/linux-s390x": "0.18.20",
"@esbuild/linux-x64": "0.18.20",
"@esbuild/netbsd-x64": "0.18.20",
"@esbuild/openbsd-x64": "0.18.20",
"@esbuild/sunos-x64": "0.18.20",
"@esbuild/win32-arm64": "0.18.20",
"@esbuild/win32-ia32": "0.18.20",
"@esbuild/win32-x64": "0.18.20"
}
},
"node_modules/@esbuild-kit/esm-loader": {
"version": "2.6.5",
"resolved": "https://registry.npmjs.org/@esbuild-kit/esm-loader/-/esm-loader-2.6.5.tgz",
@ -3306,9 +3717,9 @@
}
},
"node_modules/@react-native/codegen/node_modules/minimatch": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
@ -6726,9 +7137,9 @@
}
},
"node_modules/glob/node_modules/minimatch": {
"version": "10.2.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz",
"integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==",
"version": "10.2.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.2.tgz",
"integrity": "sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw==",
"license": "BlueOak-1.0.0",
"dependencies": {
"brace-expansion": "^5.0.2"
@ -8485,12 +8896,12 @@
}
},
"node_modules/minimatch": {
"version": "9.0.9",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
"integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.2"
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=16 || 14 >=14.17"
@ -9984,20 +10395,6 @@
"react-native": "*"
}
},
"node_modules/react-native-keyboard-controller": {
"version": "1.18.5",
"resolved": "https://registry.npmjs.org/react-native-keyboard-controller/-/react-native-keyboard-controller-1.18.5.tgz",
"integrity": "sha512-wbYN6Tcu3G5a05dhRYBgjgd74KqoYWuUmroLpigRg9cXy5uYo7prTMIvMgvLtARQtUF7BOtFggUnzgoBOgk0TQ==",
"license": "MIT",
"dependencies": {
"react-native-is-edge-to-edge": "^1.2.1"
},
"peerDependencies": {
"react": "*",
"react-native": "*",
"react-native-reanimated": ">=3.0.0"
}
},
"node_modules/react-native-reanimated": {
"version": "4.1.6",
"resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-4.1.6.tgz",
@ -10198,9 +10595,9 @@
}
},
"node_modules/react-native/node_modules/minimatch": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
@ -10597,9 +10994,9 @@
}
},
"node_modules/rimraf/node_modules/minimatch": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
@ -11204,9 +11601,9 @@
}
},
"node_modules/tar": {
"version": "7.5.10",
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.10.tgz",
"integrity": "sha512-8mOPs1//5q/rlkNSPcCegA6hiHJYDmSLEI8aMH/CdSQJNWztHC9WHNam5zdQlfpTwB9Xp7IBEsHfV5LKMJGVAw==",
"version": "7.5.9",
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.9.tgz",
"integrity": "sha512-BTLcK0xsDh2+PUe9F6c2TlRp4zOOBMTkoQHQIWSIzI0R7KG46uEwq4OPk2W7bZcprBMsuaeFsqwYr7pjh6CuHg==",
"license": "BlueOak-1.0.0",
"dependencies": {
"@isaacs/fs-minipass": "^4.0.0",
@ -11323,9 +11720,9 @@
}
},
"node_modules/test-exclude/node_modules/minimatch": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"

View file

@ -1,7 +1,7 @@
{
"name": "simpl-liste",
"main": "index.js",
"version": "1.3.0",
"version": "1.2.2",
"scripts": {
"start": "expo start",
"android": "expo start --android",
@ -44,7 +44,6 @@
"react-native-android-widget": "^0.20.1",
"react-native-draggable-flatlist": "^4.0.3",
"react-native-gesture-handler": "~2.28.0",
"react-native-keyboard-controller": "1.18.5",
"react-native-reanimated": "~4.1.1",
"react-native-safe-area-context": "~5.6.0",
"react-native-screens": "~4.16.0",
@ -60,8 +59,5 @@
"tailwindcss": "^3.4.17",
"typescript": "~5.9.2"
},
"overrides": {
"esbuild": "^0.25.0"
},
"private": true
}

View file

@ -85,19 +85,18 @@ export async function getTasksByList(listId: string, filters?: TaskFilters) {
function getOrderClauses(sortBy: SortBy, sortOrder: SortOrder) {
const dir = sortOrder === 'asc' ? asc : desc;
// Always sort completed tasks to the bottom, then apply the requested sort
switch (sortBy) {
case 'priority':
return [asc(tasks.completed), dir(tasks.priority), asc(tasks.position)];
return [dir(tasks.priority), asc(tasks.position)];
case 'dueDate':
return [asc(tasks.completed), dir(tasks.dueDate), asc(tasks.position)];
return [dir(tasks.dueDate), asc(tasks.position)];
case 'title':
return [asc(tasks.completed), dir(tasks.title)];
return [dir(tasks.title)];
case 'createdAt':
return [asc(tasks.completed), dir(tasks.createdAt)];
return [dir(tasks.createdAt)];
case 'position':
default:
return [asc(tasks.completed), asc(tasks.position), desc(tasks.createdAt)];
return [asc(tasks.position), desc(tasks.createdAt)];
}
}
@ -106,7 +105,7 @@ export async function getSubtasks(parentId: string) {
.select()
.from(tasks)
.where(eq(tasks.parentId, parentId))
.orderBy(asc(tasks.completed), asc(tasks.position));
.orderBy(asc(tasks.position));
}
export async function getTaskById(id: string) {

View file

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

View file

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

View file

@ -1,13 +0,0 @@
import type { Router } from 'expo-router';
/**
* Navigate back if possible, otherwise replace with root.
* Shared between task screens to avoid duplication.
*/
export const goBack = (router: Router) => {
if (router.canGoBack()) {
router.back();
} else {
router.replace('/');
}
};

View file

@ -34,20 +34,7 @@ export async function syncWidgetData(): Promise<void> {
try {
const now = new Date();
const todayStart = startOfDay(now);
// Read widget period setting from AsyncStorage (0 = all, N = N weeks ahead)
// Coupled with useSettingsStore.ts — key 'simpl-liste-settings', path state.widgetPeriodWeeks
let widgetPeriodWeeks = 0;
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 all tasks
}
const twoWeeksEnd = endOfDay(addWeeks(now, 2));
const selectFields = {
id: tasks.id,
@ -61,20 +48,19 @@ 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 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))));
}
// Fetch tasks with due date in the next 2 weeks
const upcomingTasks = await db
.select(selectFields)
.from(tasks)
.leftJoin(lists, eq(tasks.listId, lists.id))
.where(and(...upcomingConditions))
.where(
and(
eq(tasks.completed, false),
isNull(tasks.parentId),
gte(tasks.dueDate, todayStart),
lte(tasks.dueDate, twoWeeksEnd)
)
)
.orderBy(asc(tasks.dueDate));
// Fetch overdue tasks
@ -103,7 +89,7 @@ export async function syncWidgetData(): Promise<void> {
isNull(tasks.dueDate)
)
)
.orderBy(asc(tasks.completed), asc(tasks.position));
.orderBy(asc(tasks.position));
const toWidgetTask = (t: typeof upcomingTasks[number]): WidgetTask => ({
id: t.id,
@ -131,7 +117,7 @@ export async function syncWidgetData(): Promise<void> {
.select({ id: tasks.id, title: tasks.title, completed: tasks.completed })
.from(tasks)
.where(eq(tasks.parentId, task.id))
.orderBy(asc(tasks.completed), asc(tasks.position));
.orderBy(asc(tasks.position));
task.subtasks = subs;
}
}

View file

@ -10,13 +10,11 @@ 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>()(
@ -27,13 +25,11 @@ export const useSettingsStore = create<SettingsState>()(
notificationsEnabled: true,
reminderOffset: 0,
calendarSyncEnabled: false,
widgetPeriodWeeks: 0,
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',

View file

@ -1,5 +1,5 @@
import React from 'react';
import { FlexWidget, ListWidget, TextWidget } from 'react-native-android-widget';
import { FlexWidget, TextWidget } from 'react-native-android-widget';
import type { WidgetInfo } from 'react-native-android-widget';
type HexColor = `#${string}`;
@ -380,14 +380,17 @@ function SmallWidget({ tasks, isDark }: { tasks: WidgetTask[]; isDark: boolean }
function ListWidgetContent({
tasks,
maxItems,
isDark,
expandedTaskIds,
}: {
tasks: WidgetTask[];
maxItems: number;
isDark: boolean;
expandedTaskIds: Set<string>;
}) {
const c = getColors(isDark);
const displayTasks = tasks.slice(0, maxItems);
return (
<FlexWidget
@ -412,30 +415,29 @@ function ListWidgetContent({
borderBottomWidth: 1,
borderColor: c.border,
}}
clickAction="OPEN_APP"
>
<TextWidget
text="Simpl-Liste"
style={{
fontSize: 16,
fontFamily: FONT_SEMIBOLD,
color: c.text,
}}
/>
<FlexWidget
style={{
flex: 1,
flexDirection: 'row',
alignItems: 'center',
}}
clickAction="OPEN_APP"
>
<TextWidget
text="Simpl-Liste"
style={{
fontSize: 16,
fontFamily: FONT_SEMIBOLD,
color: c.text,
}}
/>
<TextWidget
text={`${tasks.length}`}
style={{
fontSize: 13,
fontFamily: FONT_SEMIBOLD,
color: TODAY_COLOR,
marginLeft: 8,
marginRight: 4,
}}
/>
<TextWidget
@ -447,56 +449,26 @@ function ListWidgetContent({
}}
/>
</FlexWidget>
{/* Add button */}
<FlexWidget
style={{
width: 28,
height: 28,
borderRadius: 14,
backgroundColor: TODAY_COLOR,
alignItems: 'center',
justifyContent: 'center',
marginLeft: 8,
}}
clickAction="OPEN_URI"
clickActionData={{ uri: 'simplliste:///task/new' }}
>
<TextWidget
text="+"
style={{
fontSize: 18,
fontFamily: FONT_SEMIBOLD,
color: '#FFFFFF',
}}
/>
</FlexWidget>
</FlexWidget>
{/* Task list — cap at 30 items to avoid Android widget memory limits */}
{tasks.length > 0 ? (
<ListWidget
{/* Task list */}
{displayTasks.length > 0 ? (
<FlexWidget
style={{
height: 'match_parent',
flex: 1,
flexDirection: 'column',
width: 'match_parent',
}}
>
{tasks.slice(0, 30).map((task) => (
<FlexWidget
{displayTasks.map((task) => (
<TaskItemRow
key={task.id}
style={{
flexDirection: 'column',
width: 'match_parent',
}}
>
<TaskItemRow
task={task}
isDark={isDark}
isExpanded={expandedTaskIds.has(task.id)}
/>
</FlexWidget>
task={task}
isDark={isDark}
isExpanded={expandedTaskIds.has(task.id)}
/>
))}
</ListWidget>
</FlexWidget>
) : (
<FlexWidget
style={{
@ -517,6 +489,39 @@ function ListWidgetContent({
</FlexWidget>
)}
{/* Add button footer */}
<FlexWidget
style={{
flexDirection: 'row',
justifyContent: 'center',
paddingVertical: 8,
width: 'match_parent',
borderTopWidth: 1,
borderColor: c.border,
}}
clickAction="OPEN_URI"
clickActionData={{ uri: 'simplliste:///task/new' }}
>
<FlexWidget
style={{
width: 28,
height: 28,
borderRadius: 14,
backgroundColor: TODAY_COLOR,
alignItems: 'center',
justifyContent: 'center',
}}
>
<TextWidget
text="+"
style={{
fontSize: 18,
fontFamily: FONT_SEMIBOLD,
color: '#FFFFFF',
}}
/>
</FlexWidget>
</FlexWidget>
</FlexWidget>
);
}
@ -531,9 +536,11 @@ export function TaskListWidget(props: TaskListWidgetProps) {
return <SmallWidget tasks={widgetTasks} isDark={isDark} />;
}
const maxItems = widgetName === 'SimplListeLarge' ? 8 : 4;
return (
<ListWidgetContent
tasks={widgetTasks}
maxItems={maxItems}
isDark={isDark}
expandedTaskIds={expandedTaskIds}
/>