gitshark

Clone repository

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

← Commits

✨ Add webdav support

0d8478c77bfe97c5f09536e00d221de7c605450c · vvilip · 2025-11-10T14:46:50Z

Changes

7 files changed, +835 -21

MODIFY app/(tabs)/settings.tsx +102 -19
diff --git "a/app/\050tabs\051/settings.tsx" "b/app/\050tabs\051/settings.tsx"
index ab0b187..6e544d2 100644
--- "a/app/\050tabs\051/settings.tsx"
+++ "b/app/\050tabs\051/settings.tsx"
@@ -1,11 +1,11 @@
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';
4 +import { router, useFocusEffect } from 'expo-router';
5 5 import * as DocumentPicker from 'expo-document-picker';
6 6 import { ThemedView } from '@/components/themed-view';
7 7 import { ThemedText } from '@/components/themed-text';
8 -import { storageService, taskService } from '@/services';
8 +import { storageService, taskService, webdavService } from '@/services';
9 9 import { useTheme } from '@/contexts/theme-context';
10 10 import { Colors } from '@/constants/theme';
11 11 import { useColorScheme } from '@/hooks/use-color-scheme';
@@ -13,10 +13,26 @@
13 13 export default function SettingsScreen() {
14 14 const [syncing, setSyncing] = useState(false);
15 15 const [autoArchive, setAutoArchive] = useState(false);
16 + const [webdavConfigured, setWebdavConfigured] = useState(false);
17 + const [webdavInfo, setWebdavInfo] = useState<{ url: string; username: string } | null>(null);
16 18 const { themeMode, setThemeMode } = useTheme();
17 19 const colorScheme = useColorScheme();
18 20 const colors = Colors[colorScheme];
19 21
22 + useFocusEffect(
23 + React.useCallback(() => {
24 + checkWebDAVStatus();
25 + }, [])
26 + );
27 +
28 + const checkWebDAVStatus = () => {
29 + const isConfigured = webdavService.isConfigured();
30 + setWebdavConfigured(isConfigured);
31 + if (isConfigured) {
32 + setWebdavInfo(webdavService.getConfig());
33 + }
34 + };
35 +
20 36 const handleExport = async () => {
21 37 try {
22 38 await storageService.exportToFile();
@@ -126,18 +142,51 @@
126 142 };
127 143
128 144 const handleSync = async () => {
145 + if (!webdavConfigured) {
146 + Alert.alert(
147 + 'WebDAV Not Configured',
148 + 'Please configure your WebDAV/Nextcloud connection first.',
149 + [
150 + { text: 'Cancel', style: 'cancel' },
151 + { text: 'Configure', onPress: () => router.push('/webdav-setup') },
152 + ]
153 + );
154 + return;
155 + }
156 +
129 157 setSyncing(true);
130 158 try {
131 - await new Promise(resolve => setTimeout(resolve, 1000));
159 + const result = await webdavService.sync();
132 160 Alert.alert(
133 - 'Sync Coming Soon',
134 - 'WebDAV/Nextcloud sync will be available in a future version'
161 + result.success ? 'Sync Complete' : 'Sync Failed',
162 + result.message
135 163 );
164 + } catch (error) {
165 + Alert.alert('Sync Failed', 'An error occurred during sync');
136 166 } finally {
137 167 setSyncing(false);
138 168 }
139 169 };
140 170
171 + const handleDisconnectWebDAV = () => {
172 + Alert.alert(
173 + 'Disconnect WebDAV',
174 + 'This will remove your WebDAV connection. Your local data will not be affected.',
175 + [
176 + { text: 'Cancel', style: 'cancel' },
177 + {
178 + text: 'Disconnect',
179 + style: 'destructive',
180 + onPress: () => {
181 + webdavService.disconnect();
182 + checkWebDAVStatus();
183 + Alert.alert('Disconnected', 'WebDAV connection removed');
184 + },
185 + },
186 + ]
187 + );
188 + };
189 +
141 190 return (
142 191 <ThemedView style={styles.container}>
143 192 <SafeAreaView style={styles.safeArea} edges={['top']}>
@@ -233,30 +282,64 @@
233 282 <ThemedView style={styles.section}>
234 283 <ThemedText type="subtitle" style={styles.sectionTitle}>Sync</ThemedText>
235 284
236 - <TouchableOpacity
237 - style={[styles.option, { borderBottomColor: colors.border }]}
238 - onPress={handleSync}
239 - disabled={syncing}
240 - >
241 - <ThemedText style={styles.optionText}>
242 - {syncing ? 'Syncing...' : 'Sync Now'}
243 - </ThemedText>
244 - <ThemedText style={styles.optionDescription}>
245 - WebDAV/Nextcloud sync (Coming Soon)
246 - </ThemedText>
247 - </TouchableOpacity>
285 + {!webdavConfigured ? (
286 + <TouchableOpacity
287 + style={[styles.option, { borderBottomColor: colors.border }]}
288 + onPress={() => router.push('/webdav-setup')}
289 + >
290 + <ThemedText style={styles.optionText}>Configure WebDAV / Nextcloud</ThemedText>
291 + <ThemedText style={styles.optionDescription}>
292 + Set up sync with your server
293 + </ThemedText>
294 + </TouchableOpacity>
295 + ) : (
296 + <>
297 + <ThemedView style={[styles.option, { borderBottomColor: colors.border }]}>
298 + <ThemedText style={styles.optionText}>WebDAV Connected</ThemedText>
299 + <ThemedText style={styles.optionDescription}>
300 + {webdavInfo?.username}@{webdavInfo?.url.replace(/^https?:\/\//, '').split('/')[0]}
301 + </ThemedText>
302 + </ThemedView>
303 +
304 + <TouchableOpacity
305 + style={[styles.option, { borderBottomColor: colors.border }]}
306 + onPress={handleSync}
307 + disabled={syncing}
308 + >
309 + <ThemedText style={styles.optionText}>
310 + {syncing ? 'Syncing...' : 'Sync Now'}
311 + </ThemedText>
312 + <ThemedText style={styles.optionDescription}>
313 + Sync your data with the server
314 + </ThemedText>
315 + </TouchableOpacity>
316 +
317 + <TouchableOpacity
318 + style={[styles.option, { borderBottomColor: colors.border }]}
319 + onPress={handleDisconnectWebDAV}
320 + >
321 + <ThemedText style={[styles.optionText, { color: colors.danger }]}>
322 + Disconnect WebDAV
323 + </ThemedText>
324 + <ThemedText style={styles.optionDescription}>
325 + Remove server connection
326 + </ThemedText>
327 + </TouchableOpacity>
328 + </>
329 + )}
248 330
249 331 <ThemedView style={[styles.option, { borderBottomColor: colors.border }]}>
250 332 <ThemedView style={styles.optionWithSwitch}>
251 333 <ThemedView style={styles.optionTextContainer}>
252 - <ThemedText style={styles.optionText}>Auto Archive</ThemedText>
334 + <ThemedText style={styles.optionText}>Auto Sync</ThemedText>
253 335 <ThemedText style={styles.optionDescription}>
254 - Automatically archive old completed tasks
336 + Automatically sync on app start (Coming Soon)
255 337 </ThemedText>
256 338 </ThemedView>
257 339 <Switch
258 340 value={autoArchive}
259 341 onValueChange={setAutoArchive}
342 + disabled
260 343 />
261 344 </ThemedView>
262 345 </ThemedView>
MODIFY app/_layout.tsx +1 -0
diff --git a/app/_layout.tsx b/app/_layout.tsx
index f1f05f2..13831e9 100644
--- a/app/_layout.tsx
+++ b/app/_layout.tsx
@@ -21,6 +21,7 @@
21 21 <Stack.Screen name="task/[id]" options={{ headerShown: false }} />
22 22 <Stack.Screen name="project/[id]" options={{ headerShown: false }} />
23 23 <Stack.Screen name="archive" options={{ title: 'Archive', headerBackTitle: 'Back' }} />
24 + <Stack.Screen name="webdav-setup" options={{ title: 'WebDAV Setup', headerBackTitle: 'Back' }} />
24 25 <Stack.Screen name="modal" options={{ presentation: 'modal', title: 'Modal' }} />
25 26 </Stack>
26 27 <StatusBar style="auto" />
ADD app/webdav-setup.tsx +191 -0
diff --git a/app/webdav-setup.tsx b/app/webdav-setup.tsx
new file mode 100644
index 0000000..310fb5f
--- /dev/null
+++ b/app/webdav-setup.tsx
@@ -0,0 +1,191 @@
1 +import React, { useState } from 'react';
2 +import { StyleSheet, TextInput, TouchableOpacity, ScrollView, Alert } from 'react-native';
3 +import { SafeAreaView } from 'react-native-safe-area-context';
4 +import { router } from 'expo-router';
5 +import { ThemedView } from '@/components/themed-view';
6 +import { ThemedText } from '@/components/themed-text';
7 +import { webdavService } from '@/services';
8 +import { Colors } from '@/constants/theme';
9 +import { useColorScheme } from '@/hooks/use-color-scheme';
10 +
11 +export default function WebDAVSetupScreen() {
12 + const [url, setUrl] = useState('');
13 + const [username, setUsername] = useState('');
14 + const [password, setPassword] = useState('');
15 + const [loading, setLoading] = useState(false);
16 + const colorScheme = useColorScheme();
17 + const colors = Colors[colorScheme];
18 +
19 + const handleConnect = async () => {
20 + if (!url.trim() || !username.trim() || !password.trim()) {
21 + Alert.alert('Missing Information', 'Please fill in all fields');
22 + return;
23 + }
24 +
25 + setLoading(true);
26 + try {
27 + await webdavService.initialize(url.trim(), username.trim(), password.trim());
28 +
29 + Alert.alert(
30 + 'Connection Successful',
31 + 'WebDAV connection established. Would you like to sync now?',
32 + [
33 + { text: 'Later', onPress: () => router.back() },
34 + {
35 + text: 'Sync Now',
36 + onPress: async () => {
37 + const result = await webdavService.sync();
38 + Alert.alert(
39 + result.success ? 'Sync Complete' : 'Sync Failed',
40 + result.message,
41 + [{ text: 'OK', onPress: () => router.back() }]
42 + );
43 + },
44 + },
45 + ]
46 + );
47 + } catch (error) {
48 + Alert.alert(
49 + 'Connection Failed',
50 + error instanceof Error ? error.message : 'Failed to connect to WebDAV server'
51 + );
52 + } finally {
53 + setLoading(false);
54 + }
55 + };
56 +
57 + return (
58 + <ThemedView style={styles.container}>
59 + <SafeAreaView style={styles.safeArea} edges={['bottom']}>
60 + <ScrollView style={styles.scrollView} keyboardShouldPersistTaps="handled">
61 + <ThemedView style={styles.content}>
62 + <ThemedText type="subtitle" style={styles.title}>
63 + WebDAV / Nextcloud Setup
64 + </ThemedText>
65 + <ThemedText style={[styles.description, { color: colors.subtitle }]}>
66 + Connect to your Nextcloud or WebDAV server to sync your tasks across devices.
67 + </ThemedText>
68 +
69 + <ThemedView style={styles.form}>
70 + <ThemedText style={styles.label}>Server URL</ThemedText>
71 + <TextInput
72 + style={[styles.input, {
73 + backgroundColor: colors.inputBackground,
74 + borderColor: colors.inputBorder,
75 + color: colors.text,
76 + }]}
77 + placeholder="https://cloud.example.com"
78 + placeholderTextColor={colors.placeholder}
79 + value={url}
80 + onChangeText={setUrl}
81 + autoCapitalize="none"
82 + autoCorrect={false}
83 + keyboardType="url"
84 + />
85 + <ThemedText style={[styles.hint, { color: colors.subtitle }]}>
86 + For Nextcloud: https://your-domain.com
87 + </ThemedText>
88 +
89 + <ThemedText style={styles.label}>Username</ThemedText>
90 + <TextInput
91 + style={[styles.input, {
92 + backgroundColor: colors.inputBackground,
93 + borderColor: colors.inputBorder,
94 + color: colors.text,
95 + }]}
96 + placeholder="your-username"
97 + placeholderTextColor={colors.placeholder}
98 + value={username}
99 + onChangeText={setUsername}
100 + autoCapitalize="none"
101 + autoCorrect={false}
102 + />
103 +
104 + <ThemedText style={styles.label}>Password / App Password</ThemedText>
105 + <TextInput
106 + style={[styles.input, {
107 + backgroundColor: colors.inputBackground,
108 + borderColor: colors.inputBorder,
109 + color: colors.text,
110 + }]}
111 + placeholder="password"
112 + placeholderTextColor={colors.placeholder}
113 + value={password}
114 + onChangeText={setPassword}
115 + secureTextEntry
116 + autoCapitalize="none"
117 + autoCorrect={false}
118 + />
119 + <ThemedText style={[styles.hint, { color: colors.subtitle }]}>
120 + For Nextcloud, it's recommended to use an app password
121 + </ThemedText>
122 +
123 + <TouchableOpacity
124 + style={[styles.button, { backgroundColor: colors.tint }]}
125 + onPress={handleConnect}
126 + disabled={loading}
127 + >
128 + <ThemedText style={styles.buttonText}>
129 + {loading ? 'Connecting...' : 'Connect'}
130 + </ThemedText>
131 + </TouchableOpacity>
132 + </ThemedView>
133 + </ThemedView>
134 + </ScrollView>
135 + </SafeAreaView>
136 + </ThemedView>
137 + );
138 +}
139 +
140 +const styles = StyleSheet.create({
141 + container: {
142 + flex: 1,
143 + },
144 + safeArea: {
145 + flex: 1,
146 + },
147 + scrollView: {
148 + flex: 1,
149 + },
150 + content: {
151 + padding: 16,
152 + },
153 + title: {
154 + marginBottom: 8,
155 + },
156 + description: {
157 + fontSize: 14,
158 + marginBottom: 24,
159 + lineHeight: 20,
160 + },
161 + form: {
162 + gap: 16,
163 + },
164 + label: {
165 + fontSize: 16,
166 + fontWeight: '600',
167 + marginBottom: 8,
168 + },
169 + input: {
170 + borderWidth: 1,
171 + borderRadius: 8,
172 + paddingHorizontal: 16,
173 + paddingVertical: 12,
174 + fontSize: 16,
175 + },
176 + hint: {
177 + fontSize: 12,
178 + marginTop: -8,
179 + },
180 + button: {
181 + paddingVertical: 16,
182 + borderRadius: 8,
183 + alignItems: 'center',
184 + marginTop: 8,
185 + },
186 + buttonText: {
187 + color: '#fff',
188 + fontSize: 16,
189 + fontWeight: '600',
190 + },
191 +});
MODIFY package-lock.json +314 -1
diff --git a/package-lock.json b/package-lock.json
index 7d62a44..9628483 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -37,7 +37,8 @@
37 37 "react-native-safe-area-context": "~5.6.0",
38 38 "react-native-screens": "~4.16.0",
39 39 "react-native-web": "~0.21.0",
40 - "react-native-worklets": "0.5.1"
40 + "react-native-worklets": "0.5.1",
41 + "webdav": "^5.8.0"
41 42 },
42 43 "devDependencies": {
43 44 "@types/react": "~19.1.0",
@@ -1539,6 +1540,34 @@
1539 1540 "node": ">=6.9.0"
1540 1541 }
1541 1542 },
1543 + "node_modules/@buttercup/fetch": {
1544 + "version": "0.2.1",
1545 + "resolved": "https://registry.npmjs.org/@buttercup/fetch/-/fetch-0.2.1.tgz",
1546 + "integrity": "sha512-sCgECOx8wiqY8NN1xN22BqqKzXYIG2AicNLlakOAI4f0WgyLVUbAigMf8CZhBtJxdudTcB1gD5lciqi44jwJvg==",
1547 + "license": "MIT",
1548 + "optionalDependencies": {
1549 + "node-fetch": "^3.3.0"
1550 + }
1551 + },
1552 + "node_modules/@buttercup/fetch/node_modules/node-fetch": {
1553 + "version": "3.3.2",
1554 + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
1555 + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
1556 + "license": "MIT",
1557 + "optional": true,
1558 + "dependencies": {
1559 + "data-uri-to-buffer": "^4.0.0",
1560 + "fetch-blob": "^3.1.4",
1561 + "formdata-polyfill": "^4.0.10"
1562 + },
1563 + "engines": {
1564 + "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
1565 + },
1566 + "funding": {
1567 + "type": "opencollective",
1568 + "url": "https://opencollective.com/node-fetch"
1569 + }
1570 + },
1542 1571 "node_modules/@egjs/hammerjs": {
1543 1572 "version": "2.0.17",
1544 1573 "resolved": "https://registry.npmjs.org/@egjs/hammerjs/-/hammerjs-2.0.17.tgz",
@@ -4598,6 +4627,12 @@
4598 4627 "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
4599 4628 "license": "MIT"
4600 4629 },
4630 + "node_modules/base-64": {
4631 + "version": "1.0.0",
4632 + "resolved": "https://registry.npmjs.org/base-64/-/base-64-1.0.0.tgz",
4633 + "integrity": "sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==",
4634 + "license": "MIT"
4635 + },
4601 4636 "node_modules/base64-js": {
4602 4637 "version": "1.5.1",
4603 4638 "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@@ -4781,6 +4816,12 @@
4781 4816 "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
4782 4817 "license": "MIT"
4783 4818 },
4819 + "node_modules/byte-length": {
4820 + "version": "1.0.2",
4821 + "resolved": "https://registry.npmjs.org/byte-length/-/byte-length-1.0.2.tgz",
4822 + "integrity": "sha512-ovBpjmsgd/teRmgcPh23d4gJvxDoXtAzEL9xTfMU8Yc2kqCDb7L9jAG0XHl1nzuGl+h3ebCIF1i62UFyA9V/2Q==",
4823 + "license": "MIT"
4824 + },
4784 4825 "node_modules/bytes": {
4785 4826 "version": "3.1.2",
4786 4827 "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
@@ -4898,6 +4939,15 @@
4898 4939 "url": "https://github.com/chalk/chalk?sponsor=1"
4899 4940 }
4900 4941 },
4942 + "node_modules/charenc": {
4943 + "version": "0.0.2",
4944 + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz",
4945 + "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==",
4946 + "license": "BSD-3-Clause",
4947 + "engines": {
4948 + "node": "*"
4949 + }
4950 + },
4901 4951 "node_modules/chownr": {
4902 4952 "version": "3.0.0",
4903 4953 "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
@@ -5229,6 +5279,15 @@
5229 5279 "node": ">= 8"
5230 5280 }
5231 5281 },
5282 + "node_modules/crypt": {
5283 + "version": "0.0.2",
5284 + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz",
5285 + "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==",
5286 + "license": "BSD-3-Clause",
5287 + "engines": {
5288 + "node": "*"
5289 + }
5290 + },
5232 5291 "node_modules/crypto-random-string": {
5233 5292 "version": "2.0.0",
5234 5293 "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz",
@@ -5254,6 +5313,15 @@
5254 5313 "devOptional": true,
5255 5314 "license": "MIT"
5256 5315 },
5316 + "node_modules/data-uri-to-buffer": {
5317 + "version": "4.0.1",
5318 + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
5319 + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
5320 + "license": "MIT",
5321 + "engines": {
5322 + "node": ">= 12"
5323 + }
5324 + },
5257 5325 "node_modules/data-view-buffer": {
5258 5326 "version": "1.0.2",
5259 5327 "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz",
@@ -5538,6 +5606,18 @@
5538 5606 "node": ">= 0.8"
5539 5607 }
5540 5608 },
5609 + "node_modules/entities": {
5610 + "version": "6.0.1",
5611 + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
5612 + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
5613 + "license": "BSD-2-Clause",
5614 + "engines": {
5615 + "node": ">=0.12"
5616 + },
5617 + "funding": {
5618 + "url": "https://github.com/fb55/entities?sponsor=1"
5619 + }
5620 + },
5541 5621 "node_modules/env-editor": {
5542 5622 "version": "0.4.2",
5543 5623 "resolved": "https://registry.npmjs.org/env-editor/-/env-editor-0.4.2.tgz",
@@ -7009,6 +7089,24 @@
7009 7089 "dev": true,
7010 7090 "license": "MIT"
7011 7091 },
7092 + "node_modules/fast-xml-parser": {
7093 + "version": "4.5.3",
7094 + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.3.tgz",
7095 + "integrity": "sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig==",
7096 + "funding": [
7097 + {
7098 + "type": "github",
7099 + "url": "https://github.com/sponsors/NaturalIntelligence"
7100 + }
7101 + ],
7102 + "license": "MIT",
7103 + "dependencies": {
7104 + "strnum": "^1.1.1"
7105 + },
7106 + "bin": {
7107 + "fxparser": "src/cli/cli.js"
7108 + }
7109 + },
7012 7110 "node_modules/fastq": {
7013 7111 "version": "1.19.1",
7014 7112 "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
@@ -7058,6 +7156,29 @@
7058 7156 "asap": "~2.0.3"
7059 7157 }
7060 7158 },
7159 + "node_modules/fetch-blob": {
7160 + "version": "3.2.0",
7161 + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
7162 + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
7163 + "funding": [
7164 + {
7165 + "type": "github",
7166 + "url": "https://github.com/sponsors/jimmywarting"
7167 + },
7168 + {
7169 + "type": "paypal",
7170 + "url": "https://paypal.me/jimmywarting"
7171 + }
7172 + ],
7173 + "license": "MIT",
7174 + "dependencies": {
7175 + "node-domexception": "^1.0.0",
7176 + "web-streams-polyfill": "^3.0.3"
7177 + },
7178 + "engines": {
7179 + "node": "^12.20 || >= 14.13"
7180 + }
7181 + },
7061 7182 "node_modules/file-entry-cache": {
7062 7183 "version": "8.0.0",
7063 7184 "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
@@ -7206,6 +7327,18 @@
7206 7327 "url": "https://github.com/sponsors/isaacs"
7207 7328 }
7208 7329 },
7330 + "node_modules/formdata-polyfill": {
7331 + "version": "4.0.10",
7332 + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
7333 + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
7334 + "license": "MIT",
7335 + "dependencies": {
7336 + "fetch-blob": "^3.1.2"
7337 + },
7338 + "engines": {
7339 + "node": ">=12.20.0"
7340 + }
7341 + },
7209 7342 "node_modules/freeport-async": {
7210 7343 "version": "2.0.0",
7211 7344 "resolved": "https://registry.npmjs.org/freeport-async/-/freeport-async-2.0.0.tgz",
@@ -7674,6 +7807,12 @@
7674 7807 "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
7675 7808 "license": "ISC"
7676 7809 },
7810 + "node_modules/hot-patcher": {
7811 + "version": "2.0.1",
7812 + "resolved": "https://registry.npmjs.org/hot-patcher/-/hot-patcher-2.0.1.tgz",
7813 + "integrity": "sha512-ECg1JFG0YzehicQaogenlcs2qg6WsXQsxtnbr1i696u5tLUjtJdQAh0u2g0Q5YV45f263Ta1GnUJsc8WIfJf4Q==",
7814 + "license": "MIT"
7815 + },
7677 7816 "node_modules/http-errors": {
7678 7817 "version": "2.0.0",
7679 7818 "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
@@ -7931,6 +8070,12 @@
7931 8070 "url": "https://github.com/sponsors/ljharb"
7932 8071 }
7933 8072 },
8073 + "node_modules/is-buffer": {
8074 + "version": "1.1.6",
8075 + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz",
8076 + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==",
8077 + "license": "MIT"
8078 + },
7934 8079 "node_modules/is-bun-module": {
7935 8080 "version": "2.0.0",
7936 8081 "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz",
@@ -8681,6 +8826,12 @@
8681 8826 "lan-network": "dist/lan-network-cli.js"
8682 8827 }
8683 8828 },
8829 + "node_modules/layerr": {
8830 + "version": "3.0.0",
8831 + "resolved": "https://registry.npmjs.org/layerr/-/layerr-3.0.0.tgz",
8832 + "integrity": "sha512-tv754Ki2dXpPVApOrjTyRo4/QegVb9eVFq4mjqp4+NM5NaX7syQvN5BBNfV/ZpAHCEHV24XdUVrBAoka4jt3pA==",
8833 + "license": "MIT"
8834 + },
8684 8835 "node_modules/leven": {
8685 8836 "version": "3.1.0",
8686 8837 "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
@@ -9147,6 +9298,17 @@
9147 9298 "node": ">= 0.4"
9148 9299 }
9149 9300 },
9301 + "node_modules/md5": {
9302 + "version": "2.3.0",
9303 + "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz",
9304 + "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==",
9305 + "license": "BSD-3-Clause",
9306 + "dependencies": {
9307 + "charenc": "0.0.2",
9308 + "crypt": "0.0.2",
9309 + "is-buffer": "~1.1.6"
9310 + }
9311 + },
9150 9312 "node_modules/memoize-one": {
9151 9313 "version": "5.2.1",
9152 9314 "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
@@ -9662,6 +9824,32 @@
9662 9824 "integrity": "sha512-SrQrok4CATudVzBS7coSz26QRSmlK9TzzoFbeKfcPBUFPjcQM9Rqvr/DlJkOrwI/0KcgvMub1n1g5Jt9EgRn4A==",
9663 9825 "license": "MIT"
9664 9826 },
9827 + "node_modules/nested-property": {
9828 + "version": "4.0.0",
9829 + "resolved": "https://registry.npmjs.org/nested-property/-/nested-property-4.0.0.tgz",
9830 + "integrity": "sha512-yFehXNWRs4cM0+dz7QxCd06hTbWbSkV0ISsqBfkntU6TOY4Qm3Q88fRRLOddkGh2Qq6dZvnKVAahfhjcUvLnyA==",
9831 + "license": "MIT"
9832 + },
9833 + "node_modules/node-domexception": {
9834 + "version": "1.0.0",
9835 + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
9836 + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
9837 + "deprecated": "Use your platform's native DOMException instead",
9838 + "funding": [
9839 + {
9840 + "type": "github",
9841 + "url": "https://github.com/sponsors/jimmywarting"
9842 + },
9843 + {
9844 + "type": "github",
9845 + "url": "https://paypal.me/jimmywarting"
9846 + }
9847 + ],
9848 + "license": "MIT",
9849 + "engines": {
9850 + "node": ">=10.5.0"
9851 + }
9852 + },
9665 9853 "node_modules/node-fetch": {
9666 9854 "version": "2.7.0",
9667 9855 "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
@@ -10194,6 +10382,12 @@
10194 10382 "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
10195 10383 "license": "MIT"
10196 10384 },
10385 + "node_modules/path-posix": {
10386 + "version": "1.0.0",
10387 + "resolved": "https://registry.npmjs.org/path-posix/-/path-posix-1.0.0.tgz",
10388 + "integrity": "sha512-1gJ0WpNIiYcQydgg3Ed8KzvIqTsDpNwq+cjBCssvBtuTWjEqY1AW+i+OepiEMqDCzyro9B2sLAe4RBPajMYFiA==",
10389 + "license": "ISC"
10390 + },
10197 10391 "node_modules/path-scurry": {
10198 10392 "version": "1.11.1",
10199 10393 "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
@@ -10458,6 +10652,12 @@
10458 10652 "url": "https://github.com/sponsors/sindresorhus"
10459 10653 }
10460 10654 },
10655 + "node_modules/querystringify": {
10656 + "version": "2.2.0",
10657 + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
10658 + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==",
10659 + "license": "MIT"
10660 + },
10461 10661 "node_modules/queue": {
10462 10662 "version": "6.0.2",
10463 10663 "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz",
@@ -11084,6 +11284,12 @@
11084 11284 "path-parse": "^1.0.5"
11085 11285 }
11086 11286 },
11287 + "node_modules/requires-port": {
11288 + "version": "1.0.0",
11289 + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
11290 + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
11291 + "license": "MIT"
11292 + },
11087 11293 "node_modules/resolve": {
11088 11294 "version": "1.22.11",
11089 11295 "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
@@ -12027,6 +12233,18 @@
12027 12233 "url": "https://github.com/sponsors/sindresorhus"
12028 12234 }
12029 12235 },
12236 + "node_modules/strnum": {
12237 + "version": "1.1.2",
12238 + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz",
12239 + "integrity": "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==",
12240 + "funding": [
12241 + {
12242 + "type": "github",
12243 + "url": "https://github.com/sponsors/NaturalIntelligence"
12244 + }
12245 + ],
12246 + "license": "MIT"
12247 + },
12030 12248 "node_modules/structured-headers": {
12031 12249 "version": "0.4.1",
12032 12250 "resolved": "https://registry.npmjs.org/structured-headers/-/structured-headers-0.4.1.tgz",
@@ -12696,6 +12914,25 @@
12696 12914 "punycode": "^2.1.0"
12697 12915 }
12698 12916 },
12917 + "node_modules/url-join": {
12918 + "version": "5.0.0",
12919 + "resolved": "https://registry.npmjs.org/url-join/-/url-join-5.0.0.tgz",
12920 + "integrity": "sha512-n2huDr9h9yzd6exQVnH/jU5mr+Pfx08LRXXZhkLLetAMESRj+anQsTAh940iMrIetKAmry9coFuZQ2jY8/p3WA==",
12921 + "license": "MIT",
12922 + "engines": {
12923 + "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
12924 + }
12925 + },
12926 + "node_modules/url-parse": {
12927 + "version": "1.5.10",
12928 + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
12929 + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
12930 + "license": "MIT",
12931 + "dependencies": {
12932 + "querystringify": "^2.1.1",
12933 + "requires-port": "^1.0.0"
12934 + }
12935 + },
12699 12936 "node_modules/use-callback-ref": {
12700 12937 "version": "1.3.3",
12701 12938 "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz",
@@ -13013,6 +13250,82 @@
13013 13250 "defaults": "^1.0.3"
13014 13251 }
13015 13252 },
13253 + "node_modules/web-streams-polyfill": {
13254 + "version": "3.3.3",
13255 + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
13256 + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==",
13257 + "license": "MIT",
13258 + "engines": {
13259 + "node": ">= 8"
13260 + }
13261 + },
13262 + "node_modules/webdav": {
13263 + "version": "5.8.0",
13264 + "resolved": "https://registry.npmjs.org/webdav/-/webdav-5.8.0.tgz",
13265 + "integrity": "sha512-iuFG7NamJ41Oshg4930iQgfIpRrUiatPWIekeznYgEf2EOraTRcDPTjy7gIOMtkdpKTaqPk1E68NO5PAGtJahA==",
13266 + "license": "MIT",
13267 + "dependencies": {
13268 + "@buttercup/fetch": "^0.2.1",
13269 + "base-64": "^1.0.0",
13270 + "byte-length": "^1.0.2",
13271 + "entities": "^6.0.0",
13272 + "fast-xml-parser": "^4.5.1",
13273 + "hot-patcher": "^2.0.1",
13274 + "layerr": "^3.0.0",
13275 + "md5": "^2.3.0",
13276 + "minimatch": "^9.0.5",
13277 + "nested-property": "^4.0.0",
13278 + "node-fetch": "^3.3.2",
13279 + "path-posix": "^1.0.0",
13280 + "url-join": "^5.0.0",
13281 + "url-parse": "^1.5.10"
13282 + },
13283 + "engines": {
13284 + "node": ">=14"
13285 + }
13286 + },
13287 + "node_modules/webdav/node_modules/brace-expansion": {
13288 + "version": "2.0.2",
13289 + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
13290 + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
13291 + "license": "MIT",
13292 + "dependencies": {
13293 + "balanced-match": "^1.0.0"
13294 + }
13295 + },
13296 + "node_modules/webdav/node_modules/minimatch": {
13297 + "version": "9.0.5",
13298 + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
13299 + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
13300 + "license": "ISC",
13301 + "dependencies": {
13302 + "brace-expansion": "^2.0.1"
13303 + },
13304 + "engines": {
13305 + "node": ">=16 || 14 >=14.17"
13306 + },
13307 + "funding": {
13308 + "url": "https://github.com/sponsors/isaacs"
13309 + }
13310 + },
13311 + "node_modules/webdav/node_modules/node-fetch": {
13312 + "version": "3.3.2",
13313 + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
13314 + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
13315 + "license": "MIT",
13316 + "dependencies": {
13317 + "data-uri-to-buffer": "^4.0.0",
13318 + "fetch-blob": "^3.1.4",
13319 + "formdata-polyfill": "^4.0.10"
13320 + },
13321 + "engines": {
13322 + "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
13323 + },
13324 + "funding": {
13325 + "type": "opencollective",
13326 + "url": "https://opencollective.com/node-fetch"
13327 + }
13328 + },
13016 13329 "node_modules/webidl-conversions": {
13017 13330 "version": "3.0.1",
13018 13331 "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
MODIFY package.json +2 -1
diff --git a/package.json b/package.json
index ee80a25..b9f75ac 100644
--- a/package.json
+++ b/package.json
@@ -40,7 +40,8 @@
40 40 "react-native-safe-area-context": "~5.6.0",
41 41 "react-native-screens": "~4.16.0",
42 42 "react-native-web": "~0.21.0",
43 - "react-native-worklets": "0.5.1"
43 + "react-native-worklets": "0.5.1",
44 + "webdav": "^5.8.0"
44 45 },
45 46 "devDependencies": {
46 47 "@types/react": "~19.1.0",
MODIFY services/index.ts +1 -0
diff --git a/services/index.ts b/services/index.ts
index 7ddd020..3576c14 100644
--- a/services/index.ts
+++ b/services/index.ts
@@ -6,3 +6,4 @@
6 6 export { default as taskService } from './task.service';
7 7 export { default as projectService } from './project.service';
8 8 export { default as tagService } from './tag.service';
9 +export { default as webdavService } from './webdav.service';
ADD services/webdav.service.ts +224 -0
diff --git a/services/webdav.service.ts b/services/webdav.service.ts
new file mode 100644
index 0000000..f9353c5
--- /dev/null
+++ b/services/webdav.service.ts
@@ -0,0 +1,224 @@
1 +/**
2 + * WebDAV Service
3 + * Handles sync with Nextcloud/WebDAV servers
4 + */
5 +
6 +import { createClient, WebDAVClient, FileStat } from 'webdav';
7 +import { GTDData } from '../types/gtd';
8 +import storageService from './storage.service';
9 +
10 +const WEBDAV_FILE_PATH = '/taskflow-data.json';
11 +
12 +interface WebDAVConfig {
13 + url: string;
14 + username: string;
15 + password: string;
16 +}
17 +
18 +class WebDAVService {
19 + private client: WebDAVClient | null = null;
20 + private config: WebDAVConfig | null = null;
21 +
22 + /**
23 + * Initialize WebDAV client with server configuration
24 + */
25 + async initialize(url: string, username: string, password: string): Promise<void> {
26 + try {
27 + // Ensure URL ends with /remote.php/dav/files/[username] for Nextcloud
28 + let webdavUrl = url.trim();
29 +
30 + // Remove trailing slash
31 + if (webdavUrl.endsWith('/')) {
32 + webdavUrl = webdavUrl.slice(0, -1);
33 + }
34 +
35 + // If it's a Nextcloud URL without the WebDAV path, add it
36 + if (!webdavUrl.includes('/remote.php/dav')) {
37 + webdavUrl = `${webdavUrl}/remote.php/dav/files/${username}`;
38 + }
39 +
40 + this.client = createClient(webdavUrl, {
41 + username,
42 + password,
43 + });
44 +
45 + this.config = { url: webdavUrl, username, password };
46 +
47 + // Test connection
48 + await this.client.exists('/');
49 + } catch (error) {
50 + console.error('Failed to initialize WebDAV client:', error);
51 + throw new Error('Failed to connect to WebDAV server. Please check your credentials.');
52 + }
53 + }
54 +
55 + /**
56 + * Check if WebDAV is configured
57 + */
58 + isConfigured(): boolean {
59 + return this.client !== null && this.config !== null;
60 + }
61 +
62 + /**
63 + * Get current configuration (without password)
64 + */
65 + getConfig(): { url: string; username: string } | null {
66 + if (!this.config) return null;
67 + return {
68 + url: this.config.url,
69 + username: this.config.username,
70 + };
71 + }
72 +
73 + /**
74 + * Disconnect from WebDAV server
75 + */
76 + disconnect(): void {
77 + this.client = null;
78 + this.config = null;
79 + }
80 +
81 + /**
82 + * Upload local data to WebDAV server
83 + */
84 + async uploadData(): Promise<void> {
85 + if (!this.client) {
86 + throw new Error('WebDAV client not initialized');
87 + }
88 +
89 + try {
90 + const data = await storageService.getData();
91 + const jsonData = JSON.stringify(data, null, 2);
92 +
93 + await this.client.putFileContents(WEBDAV_FILE_PATH, jsonData, {
94 + overwrite: true,
95 + });
96 + } catch (error) {
97 + console.error('Failed to upload data to WebDAV:', error);
98 + throw new Error('Failed to upload data to server');
99 + }
100 + }
101 +
102 + /**
103 + * Download data from WebDAV server
104 + */
105 + async downloadData(): Promise<GTDData | null> {
106 + if (!this.client) {
107 + throw new Error('WebDAV client not initialized');
108 + }
109 +
110 + try {
111 + const exists = await this.client.exists(WEBDAV_FILE_PATH);
112 + if (!exists) {
113 + return null;
114 + }
115 +
116 + const content = await this.client.getFileContents(WEBDAV_FILE_PATH, {
117 + format: 'text',
118 + });
119 +
120 + const data: GTDData = JSON.parse(content as string);
121 + return data;
122 + } catch (error) {
123 + console.error('Failed to download data from WebDAV:', error);
124 + throw new Error('Failed to download data from server');
125 + }
126 + }
127 +
128 + /**
129 + * Get last modified timestamp of remote file
130 + */
131 + async getRemoteLastModified(): Promise<number | null> {
132 + if (!this.client) {
133 + throw new Error('WebDAV client not initialized');
134 + }
135 +
136 + try {
137 + const exists = await this.client.exists(WEBDAV_FILE_PATH);
138 + if (!exists) {
139 + return null;
140 + }
141 +
142 + const stat = (await this.client.stat(WEBDAV_FILE_PATH)) as FileStat;
143 + return new Date(stat.lastmod).getTime();
144 + } catch (error) {
145 + console.error('Failed to get remote file info:', error);
146 + return null;
147 + }
148 + }
149 +
150 + /**
151 + * Sync data with WebDAV server
152 + * Strategy: Last-write-wins with conflict detection
153 + */
154 + async sync(): Promise<{ success: boolean; message: string }> {
155 + if (!this.client) {
156 + throw new Error('WebDAV client not initialized');
157 + }
158 +
159 + try {
160 + const localData = await storageService.getData();
161 + const remoteData = await this.downloadData();
162 +
163 + // If no remote file exists, upload local data
164 + if (!remoteData) {
165 + await this.uploadData();
166 + await storageService.updateSyncMetadata();
167 + return {
168 + success: true,
169 + message: 'Initial sync completed. Local data uploaded to server.',
170 + };
171 + }
172 +
173 + // Compare timestamps to determine which is newer
174 + const localLastSync = localData.lastSync || 0;
175 + const remoteLastSync = remoteData.lastSync || 0;
176 +
177 + // If remote is newer, download and merge
178 + if (remoteLastSync > localLastSync) {
179 + await storageService.saveData(remoteData);
180 + await storageService.updateSyncMetadata();
181 + return {
182 + success: true,
183 + message: 'Sync completed. Downloaded updates from server.',
184 + };
185 + }
186 +
187 + // If local is newer or same, upload
188 + await this.uploadData();
189 + await storageService.updateSyncMetadata();
190 + return {
191 + success: true,
192 + message: 'Sync completed. Uploaded local changes to server.',
193 + };
194 + } catch (error) {
195 + console.error('Sync failed:', error);
196 + return {
197 + success: false,
198 + message: 'Sync failed. Please check your connection and try again.',
199 + };
200 + }
201 + }
202 +
203 + /**
204 + * Force push local data to server (overwrite remote)
205 + */
206 + async forcePush(): Promise<void> {
207 + await this.uploadData();
208 + await storageService.updateSyncMetadata();
209 + }
210 +
211 + /**
212 + * Force pull data from server (overwrite local)
213 + */
214 + async forcePull(): Promise<void> {
215 + const remoteData = await this.downloadData();
216 + if (!remoteData) {
217 + throw new Error('No data found on server');
218 + }
219 + await storageService.saveData(remoteData);
220 + await storageService.updateSyncMetadata();
221 + }
222 +}
223 +
224 +export default new WebDAVService();

Keyboard shortcuts

?Show this help
g hGo home
EscClose dialog