✨ Add webdav support
Changes
7 files changed, +835 -21
MODIFY
app/(tabs)/settings.tsx
+102 -19
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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();