fixxed color selector in the category settings and fixxed drag and drop
Build APK / build (push) Canceled after 2m0s

subtask
This commit is contained in:
2026-08-09 21:42:54 +02:00
parent 4b6e87c979
commit 4c3d1a118c
99 changed files with 3335 additions and 920 deletions
+31 -13
View File
@@ -3,6 +3,7 @@ import React from 'react';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { TabBarIcon } from '@/components/TabBarIcon';
import { useSettings } from '@/theme';
import { setLastVisitedTab } from '@/utils/tabHistory';
export default function TabLayout() {
const { theme } = useSettings();
@@ -12,27 +13,44 @@ export default function TabLayout() {
<Tabs
screenOptions={{
tabBarActiveTintColor: theme.accent,
tabBarInactiveTintColor: '#8E8E8E',
tabBarInactiveTintColor: theme.text,
tabBarStyle: {
backgroundColor: theme.tabBarBg,
borderTopWidth: 1,
borderTopColor: theme.border,
height: 64 + insets.bottom,
height: 68 + insets.bottom,
paddingBottom: insets.bottom,
paddingTop: 6,
elevation: 8,
shadowColor: '#000',
shadowOffset: { width: 0, height: -2 },
shadowOpacity: 0.08,
shadowRadius: 8,
},
tabBarItemStyle: {
paddingVertical: 0,
},
tabBarLabelStyle: {
fontSize: 11,
fontWeight: '500',
fontWeight: '700',
},
headerShown: false,
sceneStyle: { backgroundColor: theme.background },
}}
screenListeners={({ route }) => ({
focus: () => {
if (route.name !== 'settings') {
setLastVisitedTab(route.name);
}
},
})}
>
<Tabs.Screen
name="index"
options={{
title: 'Tasks',
tabBarIcon: ({ focused, color }) => (
<TabBarIcon name="checklist" focused={focused} color={color} />
title: 'ToDo',
tabBarIcon: ({ color, focused }) => (
<TabBarIcon name="checklist" focused={focused} color={color} size={26} />
),
}}
/>
@@ -40,8 +58,8 @@ export default function TabLayout() {
name="calendar"
options={{
title: 'Calendar',
tabBarIcon: ({ focused, color }) => (
<TabBarIcon name="calendar" focused={focused} color={color} />
tabBarIcon: ({ color, focused }) => (
<TabBarIcon name="calendar" focused={focused} color={color} size={26} />
),
}}
/>
@@ -49,8 +67,8 @@ export default function TabLayout() {
name="stats"
options={{
title: 'Stats',
tabBarIcon: ({ focused, color }) => (
<TabBarIcon name="stats" focused={focused} color={color} />
tabBarIcon: ({ color, focused }) => (
<TabBarIcon name="stats" focused={focused} color={color} size={26} />
),
}}
/>
@@ -58,11 +76,11 @@ export default function TabLayout() {
name="settings"
options={{
title: 'Settings',
tabBarIcon: ({ focused, color }) => (
<TabBarIcon name="gear" focused={focused} color={color} />
tabBarIcon: ({ color, focused }) => (
<TabBarIcon name="gear" focused={focused} color={color} size={26} />
),
}}
/>
</Tabs>
);
}
}
+60 -52
View File
@@ -1,6 +1,6 @@
import React, { useMemo, useRef, useState, useCallback, useEffect } from 'react';
import { View, Text, StyleSheet, SafeAreaView, ScrollView, TouchableOpacity, Animated, Dimensions } from 'react-native';
import { useRouter } from 'expo-router';
import { View, Text, StyleSheet, SafeAreaView, ScrollView, TouchableOpacity, Animated, Dimensions, BackHandler, KeyboardAvoidingView } from 'react-native';
import { useRouter, useFocusEffect } from 'expo-router';
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import { Header } from '@/components/Header';
import { useTasksByDate, useTasksInMonth } from '@/hooks/useTasks';
@@ -15,10 +15,10 @@ import Task from '@/models/Task';
import { format, addMonths, addDays, startOfMonth, isSameDay, isSameMonth, isToday } from 'date-fns';
import { Q } from '@nozbe/watermelondb';
import Svg, { Path, Circle } from 'react-native-svg';
import { desaturate } from '@/theme';
import type { ThemeColors } from '@/theme';
const WIDTH = Dimensions.get('window').width;
const ORANGE = '#FF7043';
const GERMAN_WEEKDAYS = ['mo', 'di', 'mi', 'do', 'fr', 'sa', 'so'];
const MONTH_NAMES = [
'January', 'February', 'March', 'April', 'May', 'June',
@@ -43,6 +43,13 @@ export default function CalendarScreen() {
const selectedDateRef = useRef(selectedDate);
selectedDateRef.current = selectedDate;
useFocusEffect(
useCallback(() => {
const sub = BackHandler.addEventListener('hardwareBackPress', () => true);
return () => sub.remove();
}, [])
);
const translateX = useRef(new Animated.Value(0)).current;
const animatingRef = useRef(false);
const gridWidthRef = useRef<number>(WIDTH);
@@ -181,17 +188,17 @@ export default function CalendarScreen() {
return (
<SafeAreaView style={[styles.container, { backgroundColor: theme.background }]}>
<GestureDetector gesture={pan}>
<View style={styles.flex}>
<Header title="Calendar" showLogo={false} />
<Header title="Calendar" showLogo={false} />
<ScrollView showsVerticalScrollIndicator={false} contentContainerStyle={styles.scrollContent}>
<Animated.View
onLayout={(e) => {
gridWidthRef.current = e.nativeEvent.layout.width;
}}
style={[styles.calendarArea, { transform: [{ translateX }] }]}
>
<KeyboardAvoidingView style={styles.kbAvoid} behavior="padding">
<ScrollView style={styles.scrollBody} showsVerticalScrollIndicator={false} contentContainerStyle={styles.scrollContent}>
<GestureDetector gesture={pan}>
<Animated.View
onLayout={(e) => {
gridWidthRef.current = e.nativeEvent.layout.width;
}}
style={[styles.calendarArea, { transform: [{ translateX }] }]}
>
<View style={styles.monthRow}>
<TouchableOpacity
onPress={() => transitionTo(-1)}
@@ -199,7 +206,7 @@ export default function CalendarScreen() {
activeOpacity={0.7}
>
<Svg width={20} height={20} viewBox="0 0 24 24">
<Path d="M15 18l-6-6 6-6" stroke={theme.textFaint} strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" fill="none" />
<Path d="M15 18l-6-6 6-6" stroke={theme.textSecondary} strokeWidth={2.5} strokeLinecap="round" strokeLinejoin="round" fill="none" />
</Svg>
</TouchableOpacity>
@@ -207,7 +214,7 @@ export default function CalendarScreen() {
<TouchableOpacity onPress={() => setMonthPickerVisible(true)} activeOpacity={0.7} style={styles.monthButton}>
<Text style={[styles.monthLabel, { color: theme.text }]}>{format(visibleMonth, 'MMMM')}</Text>
<Svg width={14} height={14} viewBox="0 0 24 24">
<Path d="M6 9l6 6 6-6" stroke={theme.textMuted} strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" fill="none" />
<Path d="M6 9l6 6 6-6" stroke={theme.textSecondary} strokeWidth={2.5} strokeLinecap="round" strokeLinejoin="round" fill="none" />
</Svg>
</TouchableOpacity>
<TouchableOpacity onPress={() => setYearPickerVisible(true)} activeOpacity={0.7} style={styles.yearButton}>
@@ -221,7 +228,7 @@ export default function CalendarScreen() {
activeOpacity={0.7}
>
<Svg width={20} height={20} viewBox="0 0 24 24">
<Path d="M9 6l6 6-6 6" stroke={theme.textFaint} strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" fill="none" />
<Path d="M9 6l6 6-6 6" stroke={theme.textSecondary} strokeWidth={2.5} strokeLinecap="round" strokeLinejoin="round" fill="none" />
</Svg>
</TouchableOpacity>
</View>
@@ -252,8 +259,9 @@ export default function CalendarScreen() {
</View>
))}
</Animated.View>
</GestureDetector>
<View style={styles.panelHeader}>
<View style={styles.panelHeader}>
<Text style={[styles.panelDate, { color: theme.text }]}>{format(selectedDate, 'EEEE, MMMM d')}</Text>
{selectedDayTasks.length > 0 && (
<Text style={[styles.panelCount, { color: theme.textFaint }]}>
@@ -276,14 +284,14 @@ export default function CalendarScreen() {
onPress={() => handleToggleComplete(task.id)}
activeOpacity={0.7}
>
<Svg width={22} height={22} viewBox="0 0 24 24">
<Svg width={30} height={30} viewBox="0 0 24 24">
{task.completed ? (
<>
<Circle cx={12} cy={12} r={10} fill={theme.accent} />
<Path d="M7 12l3 3 7-7" stroke="#FFFFFF" strokeWidth={2.5} strokeLinecap="round" strokeLinejoin="round" fill="none" />
</>
) : (
<Circle cx={12} cy={12} r={10} stroke={theme.textMuted} strokeWidth={2} fill="none" />
<Circle cx={12} cy={12} r={10} stroke={theme.textSecondary} strokeWidth={2.5} fill="none" />
)}
</Svg>
</TouchableOpacity>
@@ -303,9 +311,7 @@ export default function CalendarScreen() {
<TouchableOpacity style={styles.menuButton} onPress={() => openTaskEdit(task.id)} activeOpacity={0.7} accessibilityRole="button" accessibilityLabel={`Edit ${task.title}`}>
<Svg width={20} height={20} viewBox="0 0 24 24">
<Circle cx={12} cy={5} r={1.5} fill={theme.textMuted} />
<Circle cx={12} cy={12} r={1.5} fill={theme.textMuted} />
<Circle cx={12} cy={19} r={1.5} fill={theme.textMuted} />
<Path d="M9 6l6 6-6 6" stroke={theme.textSecondary} strokeWidth={2.5} strokeLinecap="round" strokeLinejoin="round" fill="none" />
</Svg>
</TouchableOpacity>
</View>
@@ -337,28 +343,27 @@ export default function CalendarScreen() {
)}
</ScrollView>
<QuickAddBar dueDate={selectedDate.getTime()} placeholder={`Add event for ${format(selectedDate, 'MMM d')}`} />
<QuickAddBar dueDate={selectedDate.getTime()} placeholder={`Add event for ${format(selectedDate, 'MMM d')}`} />
</KeyboardAvoidingView>
{modals(() => {})}
{modals(() => {})}
<OptionPickerModal
visible={monthPickerVisible}
title="Select Month"
options={monthOptions}
selectedValue={String(visibleMonth.getMonth())}
onSelect={handleSelectMonth}
onClose={() => setMonthPickerVisible(false)}
/>
<OptionPickerModal
visible={yearPickerVisible}
title="Select Year"
options={yearOptions}
selectedValue={String(visibleMonth.getFullYear())}
onSelect={handleSelectYear}
onClose={() => setYearPickerVisible(false)}
/>
</View>
</GestureDetector>
<OptionPickerModal
visible={monthPickerVisible}
title="Select Month"
options={monthOptions}
selectedValue={String(visibleMonth.getMonth())}
onSelect={handleSelectMonth}
onClose={() => setMonthPickerVisible(false)}
/>
<OptionPickerModal
visible={yearPickerVisible}
title="Select Year"
options={yearOptions}
selectedValue={String(visibleMonth.getFullYear())}
onSelect={handleSelectYear}
onClose={() => setYearPickerVisible(false)}
/>
</SafeAreaView>
);
}
@@ -376,7 +381,7 @@ function cellTitle(tasks: Task[]): string | null {
function cellColor(tasks: Task[], categories: { id: string; color: string }[]): string {
if (tasks.length === 0) return '#8E8E8E';
const cat = categories.find((c) => c.id === tasks[0].categoryId);
return cat?.color ?? '#8E8E8E';
return cat ? desaturate(cat.color, 0.3) : '#8E8E8E';
}
interface DayCellProps {
@@ -393,7 +398,7 @@ interface DayCellProps {
const DayCell = React.memo(function DayCell({ day, label, dotColor, selected, today, inMonth, theme, onPress }: DayCellProps) {
return (
<TouchableOpacity
style={[styles.dayCell, selected && { backgroundColor: ORANGE }]}
style={[styles.dayCell, selected && { backgroundColor: theme.accentSoft, borderColor: theme.accentBorder }]}
onPress={() => onPress(day)}
activeOpacity={0.7}
>
@@ -401,8 +406,8 @@ const DayCell = React.memo(function DayCell({ day, label, dotColor, selected, to
style={[
styles.dayNumber,
{ color: theme.text },
!inMonth && { color: theme.borderStrong },
today && !selected && { color: ORANGE },
!inMonth && { color: theme.textMuted },
today && !selected && { color: theme.accent },
selected && styles.dayNumberSelected,
]}
>
@@ -425,12 +430,15 @@ const styles = StyleSheet.create({
container: {
flex: 1,
},
flex: {
scrollContent: {
paddingBottom: 16,
flexGrow: 1,
},
scrollBody: {
flex: 1,
},
scrollContent: {
paddingBottom: 96,
flexGrow: 1,
kbAvoid: {
flex: 1,
},
calendarArea: {
paddingHorizontal: 12,
@@ -565,8 +573,8 @@ const styles = StyleSheet.create({
alignItems: 'center',
},
checkCircle: {
width: 24,
marginRight: 10,
width: 30,
marginRight: 12,
},
eventTitleTouch: {
flex: 1,
+23 -7
View File
@@ -1,5 +1,6 @@
import React from 'react';
import { View, Text, StyleSheet, SafeAreaView } from 'react-native';
import React, { useCallback } from 'react';
import { View, Text, StyleSheet, SafeAreaView, BackHandler, KeyboardAvoidingView } from 'react-native';
import { useFocusEffect } from 'expo-router';
import { Header } from '@/components/Header';
import { CategoryFilter } from '@/components/CategoryFilter';
import { TaskList } from '@/components/TaskList';
@@ -12,6 +13,13 @@ export default function TasksScreen() {
const { theme } = useSettings();
const [selectedCategory, setSelectedCategory] = React.useState<string>('all');
useFocusEffect(
useCallback(() => {
const sub = BackHandler.addEventListener('hardwareBackPress', () => true);
return () => sub.remove();
}, [])
);
if (!isReady) {
return (
<View style={[styles.container, { backgroundColor: theme.background }]}>
@@ -22,12 +30,17 @@ export default function TasksScreen() {
return (
<SafeAreaView style={[styles.container, { backgroundColor: theme.background }]}>
<Header title="TODO" showLogo={false} />
<Header title="ToDo" showLogo={false} />
<View style={styles.categoryFilterWrapper}>
<CategoryFilter selected={selectedCategory} onSelect={setSelectedCategory} />
</View>
<TaskList categoryId={selectedCategory} />
<QuickAddBar />
<KeyboardAvoidingView
style={styles.kbAvoid}
behavior="padding"
>
<TaskList categoryId={selectedCategory} />
<QuickAddBar />
</KeyboardAvoidingView>
</SafeAreaView>
);
}
@@ -37,11 +50,14 @@ const styles = StyleSheet.create({
flex: 1,
},
categoryFilterWrapper: {
height: 36,
justifyContent: 'center',
},
kbAvoid: {
flex: 1,
},
loadingContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
});
});
+23 -7
View File
@@ -1,5 +1,6 @@
import React, { useState } from 'react';
import { View, Text, StyleSheet, SafeAreaView, ScrollView, Switch, TouchableOpacity, Alert, Linking, Modal, Pressable } from 'react-native';
import React, { useState, useCallback } from 'react';
import { View, Text, StyleSheet, SafeAreaView, ScrollView, Switch, TouchableOpacity, Alert, Linking, Modal, Pressable, BackHandler } from 'react-native';
import { useFocusEffect, useRouter } from 'expo-router';
import { Header } from '@/components/Header';
import { ListItem } from '@/components/ListItem';
import { OptionPickerModal } from '@/components/OptionPickerModal';
@@ -17,8 +18,10 @@ import { getLastSyncTime } from '@/database/sync';
import Category from '@/models/Category';
import Svg, { Path } from 'react-native-svg';
import ColorWheel from '@/components/ColorWheel';
import { getLastVisitedTab, tabHref } from '@/utils/tabHistory';
export default function SettingsScreen() {
const router = useRouter();
const { theme, notifications, setNotifications, defaultCategoryId, setDefaultCategoryId, sortBy, setSortBy, reminderPreference, setReminderPreference, apiUrl, accentColor, setAccentColor, todoAheadDays, setTodoAheadDays } = useSettings();
const categories = useCategories();
const [picker, setPicker] = useState<null | 'category' | 'sort' | 'reminder' | 'todoAhead'>(null);
@@ -75,8 +78,21 @@ export default function SettingsScreen() {
refreshSyncStatus();
}, []);
useFocusEffect(
useCallback(() => {
const sub = BackHandler.addEventListener('hardwareBackPress', () => {
const target = getLastVisitedTab();
if (target && target !== 'settings') {
router.navigate(tabHref(target));
}
return true;
});
return () => sub.remove();
}, [router])
);
const defaultCategory = categories.find((c) => c.id === defaultCategoryId);
const defaultCategoryLabel = defaultCategory?.name ?? (categories.length > 0 ? categories[0].name : 'None');
const defaultCategoryLabel = defaultCategory?.name ?? 'None';
const reminderLabel = REMINDER_OPTIONS.find((o) => o.value === reminderPreference)?.label ?? 'No reminder';
const openCustomAccent = () => {
@@ -123,7 +139,7 @@ export default function SettingsScreen() {
>
<View style={[styles.addIcon, { backgroundColor: theme.accentSoft }]}>
<Svg width={16} height={16} viewBox="0 0 24 24">
<Path d="M12 5v14M5 12h14" stroke={theme.accent} strokeWidth={2} strokeLinecap="round" />
<Path d="M12 5v14M5 12h14" stroke={theme.accentText} strokeWidth={2} strokeLinecap="round" />
</Svg>
</View>
<Text style={[styles.addCategoryText, { color: theme.accent }]}>Add Category</Text>
@@ -242,8 +258,8 @@ export default function SettingsScreen() {
<OptionPickerModal
visible={picker === 'category'}
title="Default Category"
options={categories.map((c) => ({ value: c.id, label: c.name, color: c.color }))}
selectedValue={defaultCategoryId || categories[0]?.id}
options={[{ value: '', label: 'None', color: '#9E9E9E' }, ...categories.map((c) => ({ value: c.id, label: c.name, color: c.color }))]}
selectedValue={defaultCategoryId}
onSelect={handleDefaultCategory}
onClose={() => setPicker(null)}
/>
@@ -298,7 +314,7 @@ export default function SettingsScreen() {
<Text style={[styles.accentModalTitle, { color: theme.text }]}>Custom Accent Color</Text>
<TouchableOpacity onPress={() => setCustomAccentVisible(false)} hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}>
<Svg width={18} height={18} viewBox="0 0 24 24">
<Path d="M18 6L6 18M6 6l12 12" stroke={theme.textMuted} strokeWidth={2} strokeLinecap="round" />
<Path d="M18 6L6 18M6 6l12 12" stroke={theme.textSecondary} strokeWidth={2.2} strokeLinecap="round" />
</Svg>
</TouchableOpacity>
</View>
+10 -2
View File
@@ -1,5 +1,6 @@
import React from 'react';
import { View, Text, StyleSheet, SafeAreaView, ScrollView } from 'react-native';
import React, { useCallback } from 'react';
import { View, Text, StyleSheet, SafeAreaView, ScrollView, BackHandler } from 'react-native';
import { useFocusEffect } from 'expo-router';
import { Header } from '@/components/Header';
import { useStats } from '@/hooks/useStats';
import { useSettings } from '@/theme';
@@ -8,6 +9,13 @@ export default function StatsScreen() {
const { theme } = useSettings();
const stats = useStats();
useFocusEffect(
useCallback(() => {
const sub = BackHandler.addEventListener('hardwareBackPress', () => true);
return () => sub.remove();
}, [])
);
const maxDaily = Math.max(1, ...stats.daily.map((d) => d.count));
return (