Compare commits

..

12 commits

Author SHA1 Message Date
le king fu
704ca9f693 fix: bump versionCode to 6 for APK upgrade compatibility
Previous preview build (v1.2.5) used versionCode 5 via production
autoIncrement, but app.json was still at 4. Android refuses to install
an APK with a lower versionCode than the currently installed one.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 21:10:29 -04:00
le king fu
72ace1db4a chore: bump version to 1.3.0
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 20:26:10 -04:00
3cecf9ba26 Merge pull request 'fix: show all tasks in widget (#23)' (#24) from fix/simpl-liste-23-widget-task-count into master 2026-03-13 00:25:21 +00:00
le king fu
9a8bb13e97 fix: cap widget task list at 30 items to prevent memory issues
Add safety limit of 30 rendered tasks in the widget to avoid Android
memory constraints. Also add cross-reference comment for the implicit
AsyncStorage coupling with useSettingsStore.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 20:20:47 -04:00
le king fu
2e13528c6b fix: default widget period to all tasks (#23)
Change default widgetPeriodWeeks from 2 to 0 (all tasks) so that the
issue is resolved out of the box without requiring the user to discover
the new setting.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 20:10:45 -04:00
le king fu
f040ec7902 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>
2026-03-12 20:04:35 -04:00
dde33acdf2 fix: show all tasks in widget without date or count limits
Remove the 2-week date filter from widget task query so tasks with
distant due dates are included. Remove the maxItems truncation from
the scrollable ListWidget so the displayed list matches the counter.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 19:48:14 -04:00
b5e722c1f0 Merge pull request 'fix: save and back buttons not navigating away from task screen (#21)' (#22) from fix/simpl-liste-21-save-button-navigation into master 2026-03-10 01:26:05 +00:00
4c73a16302 fix: restore error handling and deduplicate goBack helper (#21)
- Replace `finally` with `catch` in [id].tsx handleSave so goBack is
  not called when updateTask/setTagsForTask fails
- Extract shared goBack helper into src/lib/navigation.ts
- Both [id].tsx and new.tsx now import goBack from the shared module

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 21:06:05 -04:00
2296126ba4 fix: use goBack helper with canGoBack fallback and reset saving state (#21)
Replace all router.back() calls with a goBack() helper that checks
router.canGoBack() first and falls back to router.replace('/') when
there is no screen to return to. In the task edit screen, change
catch to finally so the save button always resets its disabled state
even if updateTask/setTagsForTask throws.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 20:47:22 -04:00
d6a69d849b Merge pull request 'fix: restore add button in medium/large widget (#19)' (#20) from fix/simpl-liste-19-widget-add-button into master 2026-03-09 23:52:11 +00:00
594896a909 Restore add button in medium/large widget by moving it to header
The ListWidget (Android ListView) introduced for scrolling takes all
available vertical space, pushing the footer add button off-screen.
Move the + button into the header row where it remains always visible
regardless of the task list scroll state.

Related to #19

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 23:01:52 -04:00
12 changed files with 155 additions and 74 deletions

View file

@ -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",

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

@ -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">

View file

@ -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
View file

@ -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",

View file

@ -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",

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"
}
}

13
src/lib/navigation.ts Normal file
View 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('/');
}
};

View file

@ -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

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: 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,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}
/>