๐ Add swiping again
Changes
5 files changed, +157 -80
MODIFY
app/(tabs)/_layout.tsx
+84 -57
@@ -1,70 +1,97 @@
1
-import { Tabs } from 'expo-router';
1
+import { withLayoutContext } from 'expo-router';
2
+import { createMaterialTopTabNavigator } from '@react-navigation/material-top-tabs';
2
3
import { Ionicons } from '@expo/vector-icons';
4
+import { View, TouchableOpacity, StyleSheet } from 'react-native';
5
+import { useSafeAreaInsets } from 'react-native-safe-area-context';
3
6
import { useColorScheme } from '@/hooks/use-color-scheme';
4
7
import { Colors } from '@/constants/theme';
5
8
9
+const { Navigator } = createMaterialTopTabNavigator();
10
+const MaterialTopTabs = withLayoutContext(Navigator);
11
+
12
+function CustomTabBar({ state, descriptors, navigation }) {
13
+ const colorScheme = useColorScheme();
14
+ const colors = Colors[colorScheme];
15
+ const insets = useSafeAreaInsets();
16
+
17
+ const iconMap = {
18
+ index: { focused: 'mail', unfocused: 'mail-outline' },
19
+ today: { focused: 'today', unfocused: 'today-outline' },
20
+ calendar: { focused: 'calendar', unfocused: 'calendar-outline' },
21
+ projects: { focused: 'folder', unfocused: 'folder-outline' },
22
+ settings: { focused: 'settings', unfocused: 'settings-outline' },
23
+ };
24
+
25
+ return (
26
+ <View style={[styles.tabBar, {
27
+ height: 60 + insets.bottom,
28
+ paddingBottom: insets.bottom,
29
+ backgroundColor: colors.background,
30
+ borderTopWidth: 1,
31
+ borderTopColor: colors.border,
32
+ }]}>
33
+ {state.routes.map((route, index) => {
34
+ const isFocused = state.index === index;
35
+ const iconName = isFocused ? iconMap[route.name]?.focused : iconMap[route.name]?.unfocused;
36
+ const color = isFocused ? colors.tint : colors.tabIconDefault;
37
+
38
+ const onPress = () => {
39
+ const event = navigation.emit({
40
+ type: 'tabPress',
41
+ target: route.key,
42
+ canPreventDefault: true,
43
+ });
44
+
45
+ if (!isFocused && !event.defaultPrevented) {
46
+ navigation.navigate(route.name);
47
+ }
48
+ };
49
+
50
+ return (
51
+ <TouchableOpacity
52
+ key={route.key}
53
+ onPress={onPress}
54
+ style={styles.tabItem}
55
+ >
56
+ <Ionicons name={iconName} size={24} color={color} />
57
+ </TouchableOpacity>
58
+ );
59
+ })}
60
+ </View>
61
+ );
62
+}
63
+
6
64
export default function TabLayout() {
7
65
const colorScheme = useColorScheme();
8
66
const colors = Colors[colorScheme];
9
67
10
68
return (
11
- <Tabs
69
+ <MaterialTopTabs
70
+ tabBarPosition="bottom"
71
+ tabBar={(props) => <CustomTabBar {...props} />}
12
72
screenOptions={{
13
- headerShown: false,
14
- tabBarActiveTintColor: colors.tint,
15
- tabBarInactiveTintColor: colors.tabIconDefault,
16
- tabBarStyle: {
17
- backgroundColor: colors.background,
18
- borderTopColor: colors.border,
19
- borderTopWidth: 1,
20
- },
73
+ swipeEnabled: true,
74
+ animationEnabled: true,
75
+ lazy: true,
21
76
}}
77
+ initialRouteName="index"
22
78
>
23
- <Tabs.Screen
24
- name="index"
25
- options={{
26
- title: 'Inbox',
27
- tabBarIcon: ({ color, focused }) => (
28
- <Ionicons name={focused ? 'mail' : 'mail-outline'} size={24} color={color} />
29
- ),
30
- }}
31
- />
32
- <Tabs.Screen
33
- name="today"
34
- options={{
35
- title: 'Today',
36
- tabBarIcon: ({ color, focused }) => (
37
- <Ionicons name={focused ? 'today' : 'today-outline'} size={24} color={color} />
38
- ),
39
- }}
40
- />
41
- <Tabs.Screen
42
- name="calendar"
43
- options={{
44
- title: 'Calendar',
45
- tabBarIcon: ({ color, focused }) => (
46
- <Ionicons name={focused ? 'calendar' : 'calendar-outline'} size={24} color={color} />
47
- ),
48
- }}
49
- />
50
- <Tabs.Screen
51
- name="projects"
52
- options={{
53
- title: 'Projects',
54
- tabBarIcon: ({ color, focused }) => (
55
- <Ionicons name={focused ? 'folder' : 'folder-outline'} size={24} color={color} />
56
- ),
57
- }}
58
- />
59
- <Tabs.Screen
60
- name="settings"
61
- options={{
62
- title: 'Settings',
63
- tabBarIcon: ({ color, focused }) => (
64
- <Ionicons name={focused ? 'settings' : 'settings-outline'} size={24} color={color} />
65
- ),
66
- }}
67
- />
68
- </Tabs>
79
+ <MaterialTopTabs.Screen name="index" />
80
+ <MaterialTopTabs.Screen name="today" />
81
+ <MaterialTopTabs.Screen name="calendar" />
82
+ <MaterialTopTabs.Screen name="projects" />
83
+ <MaterialTopTabs.Screen name="settings" />
84
+ </MaterialTopTabs>
69
85
);
70
-}
86
+}
87
+
88
+const styles = StyleSheet.create({
89
+ tabBar: {
90
+ flexDirection: 'row',
91
+ },
92
+ tabItem: {
93
+ flex: 1,
94
+ alignItems: 'center',
95
+ justifyContent: 'center',
96
+ },
97
+});
MODIFY
package-lock.json
+31 -11
@@ -13,6 +13,7 @@
13
13
"@react-native-community/datetimepicker": "8.4.4",
14
14
"@react-navigation/bottom-tabs": "^7.4.0",
15
15
"@react-navigation/elements": "^2.6.3",
16
+ "@react-navigation/material-top-tabs": "^7.4.3",
16
17
"@react-navigation/native": "^7.1.8",
17
18
"expo": "^54.0.23",
18
19
"expo-constants": "~18.0.10",
@@ -42,6 +43,7 @@
42
43
"react-native-tab-view": "^4.2.0",
43
44
"react-native-web": "~0.21.0",
44
45
"react-native-worklets": "0.5.1",
46
+ "typescript": "~5.9.2",
45
47
"webdav": "^5.8.0"
46
48
},
47
49
"devDependencies": {
@@ -3200,9 +3202,9 @@
3200
3202
}
3201
3203
},
3202
3204
"node_modules/@react-navigation/core": {
3203
- "version": "7.13.0",
3204
- "resolved": "https://registry.npmjs.org/@react-navigation/core/-/core-7.13.0.tgz",
3205
- "integrity": "sha512-Fc/SO23HnlGnkou/z8JQUzwEMvhxuUhr4rdPTIZp/c8q1atq3k632Nfh8fEiGtk+MP1wtIvXdN2a5hBIWpLq3g==",
3205
+ "version": "7.13.1",
3206
+ "resolved": "https://registry.npmjs.org/@react-navigation/core/-/core-7.13.1.tgz",
3207
+ "integrity": "sha512-aPf1vjQhMytPC9CmJu28hT5eTaBJuqIf9T6IRICtap5HHgFLrsYizLZrg3D0H2AoPyOoijMPWzwf7VCBzfGvrg==",
3206
3208
"license": "MIT",
3207
3209
"dependencies": {
3208
3210
"@react-navigation/routers": "^7.5.1",
@@ -3219,9 +3221,9 @@
3219
3221
}
3220
3222
},
3221
3223
"node_modules/@react-navigation/elements": {
3222
- "version": "2.8.1",
3223
- "resolved": "https://registry.npmjs.org/@react-navigation/elements/-/elements-2.8.1.tgz",
3224
- "integrity": "sha512-MLmuS5kPAeAFFOylw89WGjgEFBqGj/KBK6ZrFrAOqLnTqEzk52/SO1olb5GB00k6ZUCDZKJOp1BrLXslxE6TgQ==",
3224
+ "version": "2.8.2",
3225
+ "resolved": "https://registry.npmjs.org/@react-navigation/elements/-/elements-2.8.2.tgz",
3226
+ "integrity": "sha512-K5NWIMar81oAoRAgLwrWcLpXzY2K5yG3gNU/56uyC12u+i5SyIVAv+ygP36UXvrNLzDigg8OdRSdEBb8ePqQtA==",
3225
3227
"license": "MIT",
3226
3228
"dependencies": {
3227
3229
"color": "^4.2.3",
@@ -3230,7 +3232,7 @@
3230
3232
},
3231
3233
"peerDependencies": {
3232
3234
"@react-native-masked-view/masked-view": ">= 0.2.0",
3233
- "@react-navigation/native": "^7.1.19",
3235
+ "@react-navigation/native": "^7.1.20",
3234
3236
"react": ">= 18.2.0",
3235
3237
"react-native": "*",
3236
3238
"react-native-safe-area-context": ">= 4.0.0"
@@ -3241,14 +3243,32 @@
3241
3243
}
3242
3244
}
3243
3245
},
3246
+ "node_modules/@react-navigation/material-top-tabs": {
3247
+ "version": "7.4.3",
3248
+ "resolved": "https://registry.npmjs.org/@react-navigation/material-top-tabs/-/material-top-tabs-7.4.3.tgz",
3249
+ "integrity": "sha512-CI0jJmal0grOvCRIe2aTYUowEDnLh44J+xCwZkbRnCsMNie1sqCfbGFxuOOb/OiMuPRdvi8uCrlARNgTUMsLOg==",
3250
+ "license": "MIT",
3251
+ "dependencies": {
3252
+ "@react-navigation/elements": "^2.8.2",
3253
+ "color": "^4.2.3",
3254
+ "react-native-tab-view": "^4.2.0"
3255
+ },
3256
+ "peerDependencies": {
3257
+ "@react-navigation/native": "^7.1.20",
3258
+ "react": ">= 18.2.0",
3259
+ "react-native": "*",
3260
+ "react-native-pager-view": ">= 6.0.0",
3261
+ "react-native-safe-area-context": ">= 4.0.0"
3262
+ }
3263
+ },
3244
3264
"node_modules/@react-navigation/native": {
3245
- "version": "7.1.19",
3246
- "resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-7.1.19.tgz",
3247
- "integrity": "sha512-fM7q8di4Q8sp2WUhiUWOe7bEDRyRhbzsKQOd5N2k+lHeCx3UncsRYuw4Q/KN0EovM3wWKqMMmhy/YWuEO04kgw==",
3265
+ "version": "7.1.20",
3266
+ "resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-7.1.20.tgz",
3267
+ "integrity": "sha512-15luFq+35M2IOMHgbTJ0XDkPY7gm3YlR3yQKTuOTOHs+EeAUX71DlUuqcWMRqB0tt+OT6HimDQR7OboTB0N30g==",
3248
3268
"license": "MIT",
3249
3269
"peer": true,
3250
3270
"dependencies": {
3251
- "@react-navigation/core": "^7.13.0",
3271
+ "@react-navigation/core": "^7.13.1",
3252
3272
"escape-string-regexp": "^4.0.0",
3253
3273
"fast-deep-equal": "^3.1.3",
3254
3274
"nanoid": "^3.3.11",
MODIFY
package.json
+2 -0
@@ -16,6 +16,7 @@
16
16
"@react-native-community/datetimepicker": "8.4.4",
17
17
"@react-navigation/bottom-tabs": "^7.4.0",
18
18
"@react-navigation/elements": "^2.6.3",
19
+ "@react-navigation/material-top-tabs": "^7.4.3",
19
20
"@react-navigation/native": "^7.1.8",
20
21
"expo": "^54.0.23",
21
22
"expo-constants": "~18.0.10",
@@ -45,6 +46,7 @@
45
46
"react-native-tab-view": "^4.2.0",
46
47
"react-native-web": "~0.21.0",
47
48
"react-native-worklets": "0.5.1",
49
+ "typescript": "~5.9.2",
48
50
"webdav": "^5.8.0"
49
51
},
50
52
"devDependencies": {
MODIFY
services/notification.service.ts
+29 -9
@@ -3,18 +3,23 @@
3
3
* Handles daily task notifications
4
4
*/
5
5
6
-import * as Notifications from 'expo-notifications';
7
6
import { Platform } from 'react-native';
8
7
import taskService from './task.service';
9
8
10
-// Configure notification behavior
11
-Notifications.setNotificationHandler({
12
- handleNotification: async () => ({
13
- shouldShowAlert: true,
14
- shouldPlaySound: true,
15
- shouldSetBadge: true,
16
- } as Notifications.NotificationBehavior),
17
-});
9
+// Only import notifications on native platforms
10
+let Notifications: any;
11
+if (Platform.OS !== 'web') {
12
+ Notifications = require('expo-notifications');
13
+
14
+ // Configure notification behavior
15
+ Notifications.setNotificationHandler({
16
+ handleNotification: async () => ({
17
+ shouldShowAlert: true,
18
+ shouldPlaySound: true,
19
+ shouldSetBadge: true,
20
+ }),
21
+ });
22
+}
18
23
19
24
class NotificationService {
20
25
private notificationId: string | null = null;
@@ -23,6 +28,9 @@
23
28
* Request notification permissions
24
29
*/
25
30
async requestPermissions(): Promise<boolean> {
31
+ if (Platform.OS === 'web') {
32
+ return false;
33
+ }
26
34
try {
27
35
const { status: existingStatus } = await Notifications.getPermissionsAsync();
28
36
let finalStatus = existingStatus;
@@ -58,6 +66,9 @@
58
66
* Schedule daily notification at noon
59
67
*/
60
68
async scheduleDailyNotification(): Promise<boolean> {
69
+ if (Platform.OS === 'web') {
70
+ return false;
71
+ }
61
72
try {
62
73
// Cancel existing notification if any
63
74
if (this.notificationId) {
@@ -92,6 +103,9 @@
92
103
* Schedule notification with task count
93
104
*/
94
105
async scheduleDailyNotificationWithCount(): Promise<boolean> {
106
+ if (Platform.OS === 'web') {
107
+ return false;
108
+ }
95
109
try {
96
110
const hasPermission = await this.requestPermissions();
97
111
if (!hasPermission) {
@@ -141,6 +155,9 @@
141
155
* Cancel daily notification
142
156
*/
143
157
async cancelDailyNotification(): Promise<void> {
158
+ if (Platform.OS === 'web') {
159
+ return;
160
+ }
144
161
try {
145
162
if (this.notificationId) {
146
163
await Notifications.cancelScheduledNotificationAsync(this.notificationId);
@@ -156,6 +173,9 @@
156
173
* Check if notifications are enabled
157
174
*/
158
175
async isEnabled(): Promise<boolean> {
176
+ if (Platform.OS === 'web') {
177
+ return false;
178
+ }
159
179
try {
160
180
const { status } = await Notifications.getPermissionsAsync();
161
181
return status === 'granted';
MODIFY
services/webdav.service.ts
+11 -3
@@ -5,6 +5,7 @@
5
5
6
6
import { createClient, WebDAVClient, FileStat } from 'webdav';
7
7
import AsyncStorage from '@react-native-async-storage/async-storage';
8
+import { Platform } from 'react-native';
8
9
import { GTDData } from '../types/gtd';
9
10
import storageService from './storage.service';
10
11
@@ -23,13 +24,18 @@
23
24
private initializationPromise: Promise<void> | null = null;
24
25
25
26
constructor() {
26
- this.initializationPromise = this.loadAndInitialize();
27
+ if (Platform.OS !== 'web') {
28
+ this.initializationPromise = this.loadAndInitialize();
29
+ }
27
30
}
28
31
29
32
/**
30
33
* Load stored credentials and initialize client on app start
31
34
*/
32
35
private async loadAndInitialize(): Promise<void> {
36
+ if (Platform.OS === 'web') {
37
+ return;
38
+ }
33
39
try {
34
40
const configJson = await AsyncStorage.getItem(WEBDAV_CONFIG_KEY);
35
41
if (configJson) {
@@ -78,7 +84,7 @@
78
84
await this.client.exists('/');
79
85
80
86
// Save config for persistence
81
- if (saveConfig) {
87
+ if (saveConfig && Platform.OS !== 'web') {
82
88
await AsyncStorage.setItem(WEBDAV_CONFIG_KEY, JSON.stringify(this.config));
83
89
}
84
90
}
@@ -121,7 +127,9 @@
121
127
async disconnect(): Promise<void> {
122
128
this.client = null;
123
129
this.config = null;
124
- await AsyncStorage.removeItem(WEBDAV_CONFIG_KEY);
130
+ if (Platform.OS !== 'web') {
131
+ await AsyncStorage.removeItem(WEBDAV_CONFIG_KEY);
132
+ }
125
133
}
126
134
127
135
/**