gitshark

Clone repository

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

← Commits

๐Ÿ› Add swiping again

7318cffcd874e417ca11d783d4c349d32c0df047 ยท vvilip ยท 2025-11-17T21:15:31Z

Changes

5 files changed, +157 -80

MODIFY app/(tabs)/_layout.tsx +84 -57
diff --git "a/app/\050tabs\051/_layout.tsx" "b/app/\050tabs\051/_layout.tsx"
index c44ee9f..4602d65 100644
--- "a/app/\050tabs\051/_layout.tsx"
+++ "b/app/\050tabs\051/_layout.tsx"
@@ -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 -}
\ No newline at end of file
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 +});
\ No newline at end of file
MODIFY package-lock.json +31 -11
diff --git a/package-lock.json b/package-lock.json
index c8d0f57..2c45111 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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
diff --git a/package.json b/package.json
index 682eae0..36ae26c 100644
--- a/package.json
+++ b/package.json
@@ -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
diff --git a/services/notification.service.ts b/services/notification.service.ts
index 4644106..d2babf1 100644
--- a/services/notification.service.ts
+++ b/services/notification.service.ts
@@ -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
diff --git a/services/webdav.service.ts b/services/webdav.service.ts
index e4c9af3..23dbae1 100644
--- a/services/webdav.service.ts
+++ b/services/webdav.service.ts
@@ -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 /**

Keyboard shortcuts

?Show this help
g hGo home
EscClose dialog