gitshark

Clone repository

git clone https://git.vilip.de/git/vilip/taskflow.git
git clone git@git.vilip.de:vilip/taskflow.git

← Commits

✨ Add finish animation...

0bd359571085daabf33abadde995689a04a54601 · vvilip · 2025-11-17T21:33:04Z

Changes

7 files changed, +143 -30

MODIFY app/(tabs)/calendar.tsx +10 -2
diff --git "a/app/\050tabs\051/calendar.tsx" "b/app/\050tabs\051/calendar.tsx"
index d67cb88..d46b560 100644
--- "a/app/\050tabs\051/calendar.tsx"
+++ "b/app/\050tabs\051/calendar.tsx"
@@ -102,13 +102,21 @@
102 102
103 103 if (task.completed) {
104 104 await taskService.uncompleteTask(taskId);
105 + await loadTasks();
105 106 } else {
107 + setTasks(prevTasks =>
108 + prevTasks.map(t => t.id === taskId ? { ...t, completed: true } : t)
109 + );
110 +
106 111 await taskService.completeTask(taskId);
112 +
113 + setTimeout(() => {
114 + loadTasks();
115 + }, 1700);
107 116 }
108 -
109 - await loadTasks();
110 117 } catch (error) {
111 118 Alert.alert('Error', 'Failed to update task');
119 + await loadTasks();
112 120 }
113 121 };
114 122
MODIFY app/(tabs)/index.tsx +12 -2
diff --git "a/app/\050tabs\051/index.tsx" "b/app/\050tabs\051/index.tsx"
index a611729..b7eb655 100644
--- "a/app/\050tabs\051/index.tsx"
+++ "b/app/\050tabs\051/index.tsx"
@@ -49,13 +49,23 @@
49 49
50 50 if (task.completed) {
51 51 await taskService.uncompleteTask(taskId);
52 + await loadTasks();
52 53 } else {
54 + // Optimistically update UI for animation
55 + setTasks(prevTasks =>
56 + prevTasks.map(t => t.id === taskId ? { ...t, completed: true } : t)
57 + );
58 +
53 59 await taskService.completeTask(taskId);
60 +
61 + // Wait for animation, then reload
62 + setTimeout(() => {
63 + loadTasks();
64 + }, 1700);
54 65 }
55 -
56 - await loadTasks();
57 66 } catch (error) {
58 67 Alert.alert('Error', 'Failed to update task');
68 + await loadTasks(); // Reload on error to revert optimistic update
59 69 }
60 70 };
61 71
MODIFY app/(tabs)/today.tsx +10 -2
diff --git "a/app/\050tabs\051/today.tsx" "b/app/\050tabs\051/today.tsx"
index d51c843..b708be0 100644
--- "a/app/\050tabs\051/today.tsx"
+++ "b/app/\050tabs\051/today.tsx"
@@ -37,13 +37,21 @@
37 37
38 38 if (task.completed) {
39 39 await taskService.uncompleteTask(taskId);
40 + await loadTasks();
40 41 } else {
42 + setTodayTasks(prevTasks =>
43 + prevTasks.map(t => t.id === taskId ? { ...t, completed: true } : t)
44 + );
45 +
41 46 await taskService.completeTask(taskId);
47 +
48 + setTimeout(() => {
49 + loadTasks();
50 + }, 1700);
42 51 }
43 -
44 - await loadTasks();
45 52 } catch (error) {
46 53 Alert.alert('Error', 'Failed to update task');
54 + await loadTasks();
47 55 }
48 56 };
49 57
MODIFY app/project-modal.tsx +2 -5
diff --git a/app/project-modal.tsx b/app/project-modal.tsx
index a8996f2..3545cc2 100644
--- a/app/project-modal.tsx
+++ b/app/project-modal.tsx
@@ -23,13 +23,10 @@
23 23 const projectFormRef = React.useRef<any>(null);
24 24
25 25 const handleClose = async () => {
26 - // Try to save before closing
26 + // Try to save before closing (only if there's content)
27 27 if (projectFormRef.current?.handleSave) {
28 28 const saved = await projectFormRef.current.handleSave();
29 - if (!saved && projectId === 'new') {
30 - // Don't close if save failed on new project
31 - return;
32 - }
29 + // If save returned false, it means validation failed - just close modal without saving
33 30 }
34 31
35 32 if (router.canDismiss()) {
MODIFY app/project/[id].tsx +10 -2
diff --git "a/app/project/\133id\135.tsx" "b/app/project/\133id\135.tsx"
index 8e58d98..ec4cf00 100644
--- "a/app/project/\133id\135.tsx"
+++ "b/app/project/\133id\135.tsx"
@@ -116,13 +116,21 @@
116 116
117 117 if (task.completed) {
118 118 await taskService.uncompleteTask(taskId);
119 + await loadData();
119 120 } else {
121 + setTasks(prevTasks =>
122 + prevTasks.map(t => t.id === taskId ? { ...t, completed: true } : t)
123 + );
124 +
120 125 await taskService.completeTask(taskId);
126 +
127 + setTimeout(() => {
128 + loadData();
129 + }, 1700);
121 130 }
122 -
123 - await loadData();
124 131 } catch (error) {
125 132 Alert.alert('Error', 'Failed to update task');
133 + await loadData();
126 134 }
127 135 };
128 136
MODIFY components/fab-button.tsx +1 -1
diff --git a/components/fab-button.tsx b/components/fab-button.tsx
index e0a2276..d9b7771 100644
--- a/components/fab-button.tsx
+++ b/components/fab-button.tsx
@@ -43,6 +43,6 @@
43 43 color: '#fff',
44 44 fontSize: 32,
45 45 fontWeight: '300',
46 - marginTop: -2,
46 + lineHeight: 32,
47 47 },
48 48 });
MODIFY components/task-item.tsx +98 -16
diff --git a/components/task-item.tsx b/components/task-item.tsx
index bbe0770..00b1642 100644
--- a/components/task-item.tsx
+++ b/components/task-item.tsx
@@ -1,5 +1,5 @@
1 -import React from 'react';
2 -import { StyleSheet, TouchableOpacity, View } from 'react-native';
1 +import React, { useEffect, useRef } from 'react';
2 +import { StyleSheet, TouchableOpacity, View, Animated } from 'react-native';
3 3 import { Task } from '@/types/gtd';
4 4 import { ThemedText } from './themed-text';
5 5 import { ThemedView } from './themed-view';
@@ -15,11 +15,54 @@
15 15 export function TaskItem({ task, onPress, onToggleComplete }: TaskItemProps) {
16 16 const colorScheme = useColorScheme() ?? 'light';
17 17 const colors = Colors[colorScheme];
18 + const fadeAnim = useRef(new Animated.Value(1)).current;
19 + const scaleAnim = useRef(new Animated.Value(1)).current;
20 + const strikeAnim = useRef(new Animated.Value(0)).current;
21 + const [textWidth, setTextWidth] = React.useState(0);
18 22
19 23 const priorityColor = task.priority === 'high' ? '#ef4444' :
20 24 task.priority === 'medium' ? '#f59e0b' :
21 25 colors.subtitle;
22 26
27 + useEffect(() => {
28 + if (task.completed) {
29 + // Animate strikethrough
30 + Animated.parallel([
31 + Animated.timing(strikeAnim, {
32 + toValue: 1,
33 + duration: 400,
34 + useNativeDriver: false,
35 + }),
36 + Animated.sequence([
37 + Animated.timing(scaleAnim, {
38 + toValue: 0.98,
39 + duration: 100,
40 + useNativeDriver: true,
41 + }),
42 + Animated.timing(scaleAnim, {
43 + toValue: 1,
44 + duration: 100,
45 + useNativeDriver: true,
46 + }),
47 + ])
48 + ]).start(() => {
49 + // Wait 1 second, then fade out
50 + setTimeout(() => {
51 + Animated.timing(fadeAnim, {
52 + toValue: 0,
53 + duration: 300,
54 + useNativeDriver: true,
55 + }).start();
56 + }, 1000);
57 + });
58 + } else {
59 + // Reset animations when uncompleted
60 + fadeAnim.setValue(1);
61 + scaleAnim.setValue(1);
62 + strikeAnim.setValue(0);
63 + }
64 + }, [task.completed]);
65 +
23 66 const formatDate = (timestamp?: number) => {
24 67 if (!timestamp) return null;
25 68 const date = new Date(timestamp);
@@ -35,8 +78,19 @@
35 78 return date.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' });
36 79 };
37 80
81 + const strikeWidth = strikeAnim.interpolate({
82 + inputRange: [0, 1],
83 + outputRange: [0, textWidth],
84 + });
85 +
38 86 return (
39 - <ThemedView style={styles.container}>
87 + <Animated.View style={[
88 + styles.container,
89 + {
90 + opacity: fadeAnim,
91 + transform: [{ scale: scaleAnim }],
92 + }
93 + ]}>
40 94 <TouchableOpacity
41 95 style={[
42 96 styles.checkbox,
@@ -53,15 +107,31 @@
53 107 {task.priority && (
54 108 <View style={[styles.priorityBadge, { backgroundColor: priorityColor }]} />
55 109 )}
56 - <ThemedText
57 - style={[
58 - styles.title,
59 - task.completed && styles.completedText
60 - ]}
61 - numberOfLines={1}
62 - >
63 - {task.title}
64 - </ThemedText>
110 + <View style={styles.titleWrapper}>
111 + <View style={styles.titleContainer}>
112 + <ThemedText
113 + style={[
114 + styles.title,
115 + task.completed && styles.completedText
116 + ]}
117 + numberOfLines={1}
118 + onLayout={(e) => setTextWidth(e.nativeEvent.layout.width)}
119 + >
120 + {task.title}
121 + </ThemedText>
122 + {task.completed && (
123 + <Animated.View
124 + style={[
125 + styles.strikeLine,
126 + {
127 + width: strikeWidth,
128 + backgroundColor: colors.text,
129 + }
130 + ]}
131 + />
132 + )}
133 + </View>
134 + </View>
65 135 </View>
66 136
67 137 {(task.description || task.dueDate) && (
@@ -79,7 +149,7 @@
79 149 </View>
80 150 )}
81 151 </TouchableOpacity>
82 - </ThemedView>
152 + </Animated.View>
83 153 );
84 154 }
85 155
@@ -121,14 +191,26 @@
121 191 height: 8,
122 192 borderRadius: 4,
123 193 },
124 - title: {
125 - fontSize: 16,
194 + titleWrapper: {
126 195 flex: 1,
127 196 },
197 + titleContainer: {
198 + alignSelf: 'flex-start',
199 + position: 'relative',
200 + },
201 + title: {
202 + fontSize: 16,
203 + },
128 204 completedText: {
129 - textDecorationLine: 'line-through',
130 205 opacity: 0.5,
131 206 },
207 + strikeLine: {
208 + position: 'absolute',
209 + top: '50%',
210 + left: 0,
211 + height: 2,
212 + opacity: 0.6,
213 + },
132 214 meta: {
133 215 marginTop: 4,
134 216 gap: 4,

Keyboard shortcuts

?Show this help
g hGo home
EscClose dialog