gitshark

Clone repository

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

← Commits

✨ Add archive and import/export feature

139dd1f20ce90907885395e332503f1e5d0e5bee · vvilip · 2025-11-10T14:40:25Z

Changes

7 files changed, +268 -14

MODIFY app.json +2 -4
diff --git a/app.json b/app.json
index 3528136..e7e4569 100644
--- a/app.json
+++ b/app.json
@@ -14,9 +14,7 @@
14 14 "android": {
15 15 "adaptiveIcon": {
16 16 "backgroundColor": "#E6F4FE",
17 - "foregroundImage": "./assets/images/android-icon-foreground.png",
18 - "backgroundImage": "./assets/images/android-icon-background.png",
19 - "monochromeImage": "./assets/images/android-icon-monochrome.png"
17 + "foregroundImage": "./assets/images/Logo.png"
20 18 },
21 19 "edgeToEdgeEnabled": true,
22 20 "predictiveBackGestureEnabled": false,
@@ -31,7 +29,7 @@
31 29 [
32 30 "expo-splash-screen",
33 31 {
34 - "image": "./assets/images/splash-icon.png",
32 + "image": "./assets/images/Logo.png",
35 33 "imageWidth": 200,
36 34 "resizeMode": "contain",
37 35 "backgroundColor": "#ffffff",
MODIFY app/(tabs)/settings.tsx +73 -3
diff --git "a/app/\050tabs\051/settings.tsx" "b/app/\050tabs\051/settings.tsx"
index d29e82a..ab0b187 100644
--- "a/app/\050tabs\051/settings.tsx"
+++ "b/app/\050tabs\051/settings.tsx"
@@ -1,6 +1,8 @@
1 1 import React, { useState } from 'react';
2 2 import { StyleSheet, Alert, TouchableOpacity, ScrollView, Switch } from 'react-native';
3 3 import { SafeAreaView } from 'react-native-safe-area-context';
4 +import { router } from 'expo-router';
5 +import * as DocumentPicker from 'expo-document-picker';
4 6 import { ThemedView } from '@/components/themed-view';
5 7 import { ThemedText } from '@/components/themed-text';
6 8 import { storageService, taskService } from '@/services';
@@ -17,14 +19,65 @@
17 19
18 20 const handleExport = async () => {
19 21 try {
20 - const fileUri = await storageService.exportToFile();
22 + await storageService.exportToFile();
21 23 Alert.alert(
22 24 'Export Successful',
23 - `Data exported to:\n${fileUri}`,
25 + 'Your data has been exported as JSON. You can now save or share the file.',
24 26 [{ text: 'OK' }]
25 27 );
26 28 } catch (error) {
27 - Alert.alert('Export Failed', 'Failed to export data');
29 + Alert.alert('Export Failed', 'Failed to export data. Please try again.');
30 + }
31 + };
32 +
33 + const handleImport = async () => {
34 + try {
35 + const result = await DocumentPicker.getDocumentAsync({
36 + type: 'application/json',
37 + copyToCacheDirectory: true,
38 + });
39 +
40 + if (result.canceled) {
41 + return;
42 + }
43 +
44 + Alert.alert(
45 + 'Import Data',
46 + 'This will replace all existing data. Do you want to continue?',
47 + [
48 + { text: 'Cancel', style: 'cancel' },
49 + {
50 + text: 'Import',
51 + style: 'destructive',
52 + onPress: async () => {
53 + try {
54 + await storageService.importFromFile(result.assets[0].uri);
55 + Alert.alert(
56 + 'Import Successful',
57 + 'Your data has been imported successfully.',
58 + [
59 + {
60 + text: 'OK',
61 + onPress: () => {
62 + // Navigate to inbox to trigger reload
63 + router.replace('/(tabs)');
64 + }
65 + }
66 + ]
67 + );
68 + } catch (error) {
69 + Alert.alert(
70 + 'Import Failed',
71 + 'Failed to import data. Please make sure the file is a valid Taskflow export.',
72 + [{ text: 'OK' }]
73 + );
74 + }
75 + },
76 + },
77 + ]
78 + );
79 + } catch (error) {
80 + Alert.alert('Import Failed', 'Failed to select file. Please try again.');
28 81 }
29 82 };
30 83
@@ -152,6 +205,23 @@
152 205 </ThemedText>
153 206 </TouchableOpacity>
154 207
208 + <TouchableOpacity style={[styles.option, { borderBottomColor: colors.border }]} onPress={handleImport}>
209 + <ThemedText style={styles.optionText}>Import Data</ThemedText>
210 + <ThemedText style={styles.optionDescription}>
211 + Import data from JSON file
212 + </ThemedText>
213 + </TouchableOpacity>
214 +
215 + <TouchableOpacity
216 + style={[styles.option, { borderBottomColor: colors.border }]}
217 + onPress={() => router.push('/archive')}
218 + >
219 + <ThemedText style={styles.optionText}>View Archive</ThemedText>
220 + <ThemedText style={styles.optionDescription}>
221 + View all completed tasks
222 + </ThemedText>
223 + </TouchableOpacity>
224 +
155 225 <TouchableOpacity style={[styles.option, { borderBottomColor: colors.border }]} onPress={handleClearCompleted}>
156 226 <ThemedText style={styles.optionText}>Archive Completed Tasks</ThemedText>
157 227 <ThemedText style={styles.optionDescription}>
MODIFY app/_layout.tsx +1 -0
diff --git a/app/_layout.tsx b/app/_layout.tsx
index 12f1082..f1f05f2 100644
--- a/app/_layout.tsx
+++ b/app/_layout.tsx
@@ -20,6 +20,7 @@
20 20 <Stack.Screen name="(tabs)" options={{ headerShown: false }} />
21 21 <Stack.Screen name="task/[id]" options={{ headerShown: false }} />
22 22 <Stack.Screen name="project/[id]" options={{ headerShown: false }} />
23 + <Stack.Screen name="archive" options={{ title: 'Archive', headerBackTitle: 'Back' }} />
23 24 <Stack.Screen name="modal" options={{ presentation: 'modal', title: 'Modal' }} />
24 25 </Stack>
25 26 <StatusBar style="auto" />
ADD app/archive.tsx +138 -0
diff --git a/app/archive.tsx b/app/archive.tsx
new file mode 100644
index 0000000..e7974c4
--- /dev/null
+++ b/app/archive.tsx
@@ -0,0 +1,138 @@
1 +import React, { useState } from 'react';
2 +import { StyleSheet, FlatList } from 'react-native';
3 +import { ThemedView } from '@/components/themed-view';
4 +import { ThemedText } from '@/components/themed-text';
5 +import { TaskItem } from '@/components/task-item';
6 +import { Task } from '@/types/gtd';
7 +import { taskService } from '@/services';
8 +import { Colors } from '@/constants/theme';
9 +import { useColorScheme } from '@/hooks/use-color-scheme';
10 +import { router } from 'expo-router';
11 +
12 +export default function ArchiveScreen() {
13 + const [tasks, setTasks] = useState<Task[]>([]);
14 + const [loading, setLoading] = useState(true);
15 + const colorScheme = useColorScheme();
16 + const colors = Colors[colorScheme];
17 +
18 + React.useEffect(() => {
19 + loadCompletedTasks();
20 + }, []);
21 +
22 + const loadCompletedTasks = async () => {
23 + try {
24 + const completedTasks = await taskService.getCompletedTasks();
25 + setTasks(completedTasks);
26 + } catch (error) {
27 + console.error('Failed to load completed tasks:', error);
28 + } finally {
29 + setLoading(false);
30 + }
31 + };
32 +
33 + const handleTaskPress = (task: Task) => {
34 + router.push(`/task/${task.id}`);
35 + };
36 +
37 + const handleToggleComplete = async (taskId: string) => {
38 + try {
39 + await taskService.uncompleteTask(taskId);
40 + await loadCompletedTasks();
41 + } catch (error) {
42 + console.error('Failed to uncomplete task:', error);
43 + }
44 + };
45 +
46 + const formatDate = (timestamp?: number) => {
47 + if (!timestamp) return '';
48 + const date = new Date(timestamp);
49 + const now = new Date();
50 + const diffInDays = Math.floor((now.getTime() - date.getTime()) / (1000 * 60 * 60 * 24));
51 +
52 + if (diffInDays === 0) return 'Today';
53 + if (diffInDays === 1) return 'Yesterday';
54 + if (diffInDays < 7) return `${diffInDays} days ago`;
55 + if (diffInDays < 30) return `${Math.floor(diffInDays / 7)} weeks ago`;
56 + return `${Math.floor(diffInDays / 30)} months ago`;
57 + };
58 +
59 + if (loading) {
60 + return (
61 + <ThemedView style={styles.container}>
62 + <ThemedView style={styles.emptyState}>
63 + <ThemedText>Loading...</ThemedText>
64 + </ThemedView>
65 + </ThemedView>
66 + );
67 + }
68 +
69 + if (tasks.length === 0) {
70 + return (
71 + <ThemedView style={styles.container}>
72 + <ThemedView style={styles.emptyState}>
73 + <ThemedText style={styles.emptyTitle}>No Completed Tasks</ThemedText>
74 + <ThemedText style={styles.emptyDescription}>
75 + Completed tasks will appear here
76 + </ThemedText>
77 + </ThemedView>
78 + </ThemedView>
79 + );
80 + }
81 +
82 + return (
83 + <ThemedView style={styles.container}>
84 + <FlatList
85 + data={tasks}
86 + keyExtractor={(item) => item.id}
87 + contentContainerStyle={styles.listContent}
88 + renderItem={({ item }) => (
89 + <ThemedView>
90 + <TaskItem
91 + task={item}
92 + onPress={() => handleTaskPress(item)}
93 + onToggleComplete={() => handleToggleComplete(item.id)}
94 + />
95 + {item.completedAt && (
96 + <ThemedText style={[styles.completedDate, { color: colors.subtitle }]}>
97 + Completed {formatDate(item.completedAt)}
98 + </ThemedText>
99 + )}
100 + </ThemedView>
101 + )}
102 + />
103 + </ThemedView>
104 + );
105 +}
106 +
107 +const styles = StyleSheet.create({
108 + container: {
109 + flex: 1,
110 + },
111 + listContent: {
112 + paddingHorizontal: 16,
113 + paddingTop: 8,
114 + paddingBottom: 32,
115 + },
116 + completedDate: {
117 + fontSize: 12,
118 + paddingHorizontal: 16,
119 + paddingBottom: 12,
120 + marginTop: -8,
121 + },
122 + emptyState: {
123 + flex: 1,
124 + justifyContent: 'center',
125 + alignItems: 'center',
126 + paddingHorizontal: 32,
127 + },
128 + emptyTitle: {
129 + fontSize: 20,
130 + fontWeight: '600',
131 + marginBottom: 8,
132 + },
133 + emptyDescription: {
134 + fontSize: 16,
135 + opacity: 0.6,
136 + textAlign: 'center',
137 + },
138 +});
MODIFY package-lock.json +20 -0
diff --git a/package-lock.json b/package-lock.json
index 8d41ed9..7d62a44 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -16,12 +16,14 @@
16 16 "@react-navigation/native": "^7.1.8",
17 17 "expo": "~54.0.22",
18 18 "expo-constants": "~18.0.10",
19 + "expo-document-picker": "^14.0.7",
19 20 "expo-file-system": "^19.0.17",
20 21 "expo-font": "~14.0.9",
21 22 "expo-haptics": "~15.0.7",
22 23 "expo-image": "~3.0.10",
23 24 "expo-linking": "~8.0.8",
24 25 "expo-router": "~6.0.14",
26 + "expo-sharing": "^14.0.7",
25 27 "expo-splash-screen": "~31.0.10",
26 28 "expo-status-bar": "~3.0.8",
27 29 "expo-symbols": "~1.0.7",
@@ -6278,6 +6280,15 @@
6278 6280 "react-native": "*"
6279 6281 }
6280 6282 },
6283 + "node_modules/expo-document-picker": {
6284 + "version": "14.0.7",
6285 + "resolved": "https://registry.npmjs.org/expo-document-picker/-/expo-document-picker-14.0.7.tgz",
6286 + "integrity": "sha512-81Jh8RDD0GYBUoSTmIBq30hXXjmkDV1ZY2BNIp1+3HR5PDSh2WmdhD/Ezz5YFsv46hIXHsQc+Kh1q8vn6OLT9Q==",
6287 + "license": "MIT",
6288 + "peerDependencies": {
6289 + "expo": "*"
6290 + }
6291 + },
6281 6292 "node_modules/expo-file-system": {
6282 6293 "version": "19.0.17",
6283 6294 "resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-19.0.17.tgz",
@@ -6645,6 +6656,15 @@
6645 6656 "node": ">=20.16.0"
6646 6657 }
6647 6658 },
6659 + "node_modules/expo-sharing": {
6660 + "version": "14.0.7",
6661 + "resolved": "https://registry.npmjs.org/expo-sharing/-/expo-sharing-14.0.7.tgz",
6662 + "integrity": "sha512-t/5tR8ZJNH6tMkHXlF7453UafNIfrpfTG+THN9EMLC4Wsi4bJuESPm3NdmWDg2D4LDALJI/LQo0iEnLAd5Sp4g==",
6663 + "license": "MIT",
6664 + "peerDependencies": {
6665 + "expo": "*"
6666 + }
6667 + },
6648 6668 "node_modules/expo-splash-screen": {
6649 6669 "version": "31.0.10",
6650 6670 "resolved": "https://registry.npmjs.org/expo-splash-screen/-/expo-splash-screen-31.0.10.tgz",
MODIFY package.json +2 -0
diff --git a/package.json b/package.json
index 8d5d556..ee80a25 100644
--- a/package.json
+++ b/package.json
@@ -19,12 +19,14 @@
19 19 "@react-navigation/native": "^7.1.8",
20 20 "expo": "~54.0.22",
21 21 "expo-constants": "~18.0.10",
22 + "expo-document-picker": "^14.0.7",
22 23 "expo-file-system": "^19.0.17",
23 24 "expo-font": "~14.0.9",
24 25 "expo-haptics": "~15.0.7",
25 26 "expo-image": "~3.0.10",
26 27 "expo-linking": "~8.0.8",
27 28 "expo-router": "~6.0.14",
29 + "expo-sharing": "^14.0.7",
28 30 "expo-splash-screen": "~31.0.10",
29 31 "expo-status-bar": "~3.0.8",
30 32 "expo-symbols": "~1.0.7",
MODIFY services/storage.service.ts +32 -7
diff --git a/services/storage.service.ts b/services/storage.service.ts
index 56127db..c4ef69b 100644
--- a/services/storage.service.ts
+++ b/services/storage.service.ts
@@ -5,7 +5,8 @@
5 5 */
6 6
7 7 import AsyncStorage from '@react-native-async-storage/async-storage';
8 -import * as FileSystem from 'expo-file-system';
8 +import { Paths, File } from 'expo-file-system';
9 +import * as Sharing from 'expo-sharing';
9 10 import { GTDData, Task, Project, Tag, SyncMetadata } from '../types/gtd';
10 11
11 12 const STORAGE_KEY = '@taskflow_gtd_data';
@@ -77,7 +78,7 @@
77 78 }
78 79
79 80 /**
80 - * Export data as JSON file to device storage
81 + * Export data as JSON file and allow sharing/downloading
81 82 */
82 83 async exportToFile(): Promise<string> {
83 84 try {
@@ -86,13 +87,20 @@
86 87
87 88 const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
88 89 const filename = `taskflow-export-${timestamp}.json`;
89 - const fileUri = `${FileSystem.documentDirectory}${filename}`;
90 + const file = new File(Paths.cache, filename);
90 91
91 - await FileSystem.writeAsStringAsync(fileUri, jsonData, {
92 - encoding: FileSystem.EncodingType.UTF8,
93 - });
92 + await file.write(jsonData);
94 93
95 - return fileUri;
94 + const isSharingAvailable = await Sharing.isAvailableAsync();
95 + if (isSharingAvailable) {
96 + await Sharing.shareAsync(file.uri, {
97 + mimeType: 'application/json',
98 + dialogTitle: 'Export Taskflow Data',
99 + UTI: 'public.json',
100 + });
101 + }
102 +
103 + return file.uri;
96 104 } catch (error) {
97 105 console.error('Error exporting data:', error);
98 106 throw new Error('Failed to export data');
@@ -111,6 +119,9 @@
111 119 throw new Error('Invalid data format');
112 120 }
113 121
122 + // Ensure version is set
123 + data.version = data.version || STORAGE_VERSION;
124 +
114 125 await this.saveData(data);
115 126 } catch (error) {
116 127 console.error('Error importing data:', error);
@@ -119,6 +130,20 @@
119 130 }
120 131
121 132 /**
133 + * Import data from a file URI
134 + */
135 + async importFromFile(fileUri: string): Promise<void> {
136 + try {
137 + const file = new File(fileUri);
138 + const content = await file.text();
139 + await this.importFromJson(content);
140 + } catch (error) {
141 + console.error('Error importing from file:', error);
142 + throw new Error('Failed to import data from file');
143 + }
144 + }
145 +
146 + /**
122 147 * Calculate hash for sync conflict detection
123 148 */
124 149 private calculateHash(data: GTDData): string {

Keyboard shortcuts

?Show this help
g hGo home
EscClose dialog