Compare commits

..

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

12 changed files with 74 additions and 155 deletions

View file

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

View file

@ -2,14 +2,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 { 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 +23,6 @@ export default function SettingsScreen() {
notificationsEnabled, setNotificationsEnabled,
reminderOffset, setReminderOffset,
calendarSyncEnabled, setCalendarSyncEnabled,
widgetPeriodWeeks, setWidgetPeriodWeeks,
} = useSettingsStore();
const isDark = (theme === 'system' ? systemScheme : theme) === 'dark';
@ -301,56 +299,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">

View file

@ -25,7 +25,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,
@ -127,9 +126,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 +141,7 @@ export default function TaskDetailScreen() {
onPress: async () => {
await deleteTask(id!);
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
goBack(router);
router.back();
},
},
]);
@ -187,7 +185,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-2.5" hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}>
<ArrowLeft size={24} color={isDark ? '#F5F5F5' : '#1A1A1A'} />
</Pressable>
<View className="flex-row items-center">

View file

@ -27,7 +27,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> = {
@ -84,7 +83,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 +120,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-2.5" hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}>
<X size={24} color={isDark ? '#F5F5F5' : '#1A1A1A'} />
</Pressable>
<Text

4
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "simpl-liste",
"version": "1.3.0",
"version": "1.2.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "simpl-liste",
"version": "1.3.0",
"version": "1.2.4",
"dependencies": {
"@expo-google-fonts/inter": "^0.4.2",
"@expo/ngrok": "^4.1.3",

View file

@ -1,7 +1,7 @@
{
"name": "simpl-liste",
"main": "index.js",
"version": "1.3.0",
"version": "1.2.5",
"scripts": {
"start": "expo start",
"android": "expo start --android",

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

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

@ -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,41 +449,17 @@ 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 ? (
{/* Task list */}
{displayTasks.length > 0 ? (
<ListWidget
style={{
height: 'match_parent',
width: 'match_parent',
}}
>
{tasks.slice(0, 30).map((task) => (
{displayTasks.map((task) => (
<FlexWidget
key={task.id}
style={{
@ -517,6 +495,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 +542,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}
/>