Compare commits
12 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
704ca9f693 | ||
|
|
72ace1db4a | ||
| 3cecf9ba26 | |||
|
|
9a8bb13e97 | ||
|
|
2e13528c6b | ||
|
|
f040ec7902 | ||
| dde33acdf2 | |||
| b5e722c1f0 | |||
| 4c73a16302 | |||
| 2296126ba4 | |||
| d6a69d849b | |||
| 594896a909 |
12 changed files with 155 additions and 74 deletions
4
app.json
4
app.json
|
|
@ -2,7 +2,7 @@
|
|||
"expo": {
|
||||
"name": "Simpl-Liste",
|
||||
"slug": "simpl-liste",
|
||||
"version": "1.2.5",
|
||||
"version": "1.3.0",
|
||||
"orientation": "portrait",
|
||||
"icon": "./assets/images/icon.png",
|
||||
"scheme": "simplliste",
|
||||
|
|
@ -24,7 +24,7 @@
|
|||
"backgroundColor": "#FFF8F0"
|
||||
},
|
||||
"edgeToEdgeEnabled": true,
|
||||
"versionCode": 4
|
||||
"versionCode": 6
|
||||
},
|
||||
"plugins": [
|
||||
"expo-router",
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ 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,
|
||||
|
|
@ -126,8 +127,9 @@ export default function TaskDetailScreen() {
|
|||
listId: selectedListId,
|
||||
});
|
||||
await setTagsForTask(task.id, selectedTagIds);
|
||||
router.back();
|
||||
goBack(router);
|
||||
} catch {
|
||||
// Save failed — stay on screen so user can retry
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
|
@ -141,7 +143,7 @@ export default function TaskDetailScreen() {
|
|||
onPress: async () => {
|
||||
await deleteTask(id!);
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
|
||||
router.back();
|
||||
goBack(router);
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
|
@ -185,7 +187,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={() => router.back()} className="p-2.5" hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}>
|
||||
<Pressable onPress={() => goBack(router)} 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">
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ 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> = {
|
||||
|
|
@ -83,7 +84,7 @@ export default function NewTaskScreen() {
|
|||
for (const sub of pendingSubtasks) {
|
||||
await createTask({ title: sub, listId: selectedListId, parentId: taskId });
|
||||
}
|
||||
router.back();
|
||||
goBack(router);
|
||||
} catch {
|
||||
// FK constraint or other DB error — fallback to inbox
|
||||
setSaving(false);
|
||||
|
|
@ -120,7 +121,7 @@ export default function NewTaskScreen() {
|
|||
isDark ? 'border-[#3A3A3A]' : 'border-[#E5E7EB]'
|
||||
}`}
|
||||
>
|
||||
<Pressable onPress={() => router.back()} className="p-2.5" hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}>
|
||||
<Pressable onPress={() => goBack(router)} 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
4
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "simpl-liste",
|
||||
"version": "1.2.4",
|
||||
"version": "1.3.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "simpl-liste",
|
||||
"version": "1.2.4",
|
||||
"version": "1.3.0",
|
||||
"dependencies": {
|
||||
"@expo-google-fonts/inter": "^0.4.2",
|
||||
"@expo/ngrok": "^4.1.3",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "simpl-liste",
|
||||
"main": "index.js",
|
||||
"version": "1.2.5",
|
||||
"version": "1.3.0",
|
||||
"scripts": {
|
||||
"start": "expo start",
|
||||
"android": "expo start --android",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
13
src/lib/navigation.ts
Normal file
13
src/lib/navigation.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
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('/');
|
||||
}
|
||||
};
|
||||
|
|
@ -34,7 +34,20 @@ export async function syncWidgetData(): Promise<void> {
|
|||
try {
|
||||
const now = new Date();
|
||||
const todayStart = startOfDay(now);
|
||||
const twoWeeksEnd = endOfDay(addWeeks(now, 2));
|
||||
|
||||
// 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 selectFields = {
|
||||
id: tasks.id,
|
||||
|
|
@ -48,19 +61,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 tasks with due date in the next 2 weeks
|
||||
// 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),
|
||||
lte(tasks.dueDate, twoWeeksEnd)
|
||||
)
|
||||
)
|
||||
.where(and(...upcomingConditions))
|
||||
.orderBy(asc(tasks.dueDate));
|
||||
|
||||
// Fetch overdue tasks
|
||||
|
|
|
|||
|
|
@ -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: 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',
|
||||
|
|
|
|||
|
|
@ -380,17 +380,14 @@ 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
|
||||
|
|
@ -415,29 +412,30 @@ 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,
|
||||
marginRight: 4,
|
||||
marginLeft: 8,
|
||||
}}
|
||||
/>
|
||||
<TextWidget
|
||||
|
|
@ -449,17 +447,41 @@ 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 */}
|
||||
{displayTasks.length > 0 ? (
|
||||
{/* Task list — cap at 30 items to avoid Android widget memory limits */}
|
||||
{tasks.length > 0 ? (
|
||||
<ListWidget
|
||||
style={{
|
||||
height: 'match_parent',
|
||||
width: 'match_parent',
|
||||
}}
|
||||
>
|
||||
{displayTasks.map((task) => (
|
||||
{tasks.slice(0, 30).map((task) => (
|
||||
<FlexWidget
|
||||
key={task.id}
|
||||
style={{
|
||||
|
|
@ -495,39 +517,6 @@ 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -542,11 +531,9 @@ 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}
|
||||
/>
|
||||
|
|
|
|||
Loading…
Reference in a new issue