Compare commits

..
17 Commits
Author SHA1 Message Date
tech08mag f6abf628e3 Merge pull request 'new release pipe' (#29) from dev into main
Build and Release APK / build (push) Failing after 3m48s
Build and Release APK / release (push) Skipped
Reviewed-on: #29
2026-08-28 20:00:40 +00:00
tech08mag 40077da57f new release pipe 2026-08-28 22:00:13 +02:00
tech08mag aa57d07dc0 Merge pull request 'gradle update' (#28) from dev into main
Build APK / build (push) Successful in 17m50s
release / release (push) Canceled after 0s
Reviewed-on: #28
2026-08-28 19:39:21 +00:00
tech08mag 89c85ed7c0 gradle update 2026-08-28 21:38:23 +02:00
tech08mag ee6af8fb26 Merge pull request 'fixxing pipe dependencies' (#27) from dev into main
Build APK / build (push) Failing after 1m36s
release / release (push) Canceled after 0s
Reviewed-on: #27
2026-08-28 19:35:16 +00:00
tech08mag c6e441d449 fixxing pipe dependencies 2026-08-28 21:34:14 +02:00
tech08mag 5043d9cdff Merge pull request 'fixxing android pipe' (#26) from dev into main
Build APK / build (push) Failing after 48s
release / release (push) Canceled after 0s
Reviewed-on: #26
2026-08-28 19:30:31 +00:00
tech08mag 7e36244478 fixxing android pipe 2026-08-28 21:29:12 +02:00
tech08mag 157fc8d95b Merge pull request 'pipe' (#24) from dev into main
Build APK / build (push) Failing after 2m55s
release / release (push) Canceled after 0s
Reviewed-on: #24
2026-08-27 18:42:32 +00:00
tech08mag 4d2f92b144 pipe 2026-08-27 20:41:31 +02:00
tech08mag 8e805316fb Merge pull request 'new pipe' (#23) from dev into main
Build Desktop (deb) / build (push) Failing after 5m18s
Build APK / build (push) Successful in 15m35s
release / release (push) Canceled after 0s
Reviewed-on: #23
2026-08-27 18:04:54 +00:00
tech08mag 77051e5206 new pipe 2026-08-27 20:03:05 +02:00
tech08mag 7e06415db4 Merge pull request 'Dev' (#22) from dev into main
Build Desktop (deb) / build (push) Failing after 5m27s
release / release (push) Successful in 6m5s
Build APK / build (push) Successful in 15m43s
Reviewed-on: #22
2026-08-27 04:42:02 +00:00
tech08mag cfb987a305 Fix OutOfMemoryError: increase MaxMetaspaceSize from 384m to 1024m 2026-08-27 06:35:06 +02:00
tech08mag 3dad9e93ec layout 2026-08-26 23:48:42 +02:00
tech08mag 2be4d5aa4e Merge pull request 'keystore key' (#21) from dev into main
Build Desktop (deb) / build (push) Failing after 5m14s
release / release (push) Successful in 6m7s
Build APK / build (push) Failing after 3h0m11s
Reviewed-on: #21
2026-08-26 21:21:00 +00:00
tech08mag e91251759b keystore key 2026-08-26 23:20:19 +02:00
19 changed files with 310 additions and 198 deletions
-75
View File
@@ -1,75 +0,0 @@
name: Build APK
on:
push:
branches: [main]
workflow_dispatch:
env:
ANDROID_HOME: ${{ runner.tool_cache }}/android-sdk
ANDROID_SDK_ROOT: ${{ runner.tool_cache }}/android-sdk
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: 22
- name: Set up JDK
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 17
- name: Install Android SDK
run: |
if [ -x "$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" ]; then
echo "Android SDK already present in cache"
else
rm -rf "$ANDROID_HOME/cmdline-tools"
mkdir -p "$ANDROID_HOME/cmdline-tools"
wget -q https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip -O /tmp/cmdtools.zip
unzip -q /tmp/cmdtools.zip -d "$ANDROID_HOME/cmdline-tools"
mv "$ANDROID_HOME/cmdline-tools/cmdline-tools" "$ANDROID_HOME/cmdline-tools/latest"
fi
printf 'y\n' | "$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" --licenses > /dev/null
"$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" \
"platform-tools" \
"platforms;android-35" \
"platforms;android-36" \
"build-tools;35.0.0" \
"build-tools;36.0.0" \
"ndk;27.1.12297006"
mkdir -p carry-your-live/android/app
echo "sdk.dir=$ANDROID_HOME" > carry-your-live/android/app/local.properties
- name: Install JS dependencies
working-directory: carry-your-live
run: npm ci
- name: Generate debug keystore (signs the release APK)
run: |
mkdir -p carry-your-live/android/app
rm -f carry-your-live/android/app/debug.keystore
keytool -genkeypair -v \
-keystore carry-your-live/android/app/debug.keystore \
-alias androiddebugkey -storepass android -keypass android \
-keyalg RSA -keysize 2048 -validity 10000 \
-dname "CN=Android Debug,O=Android,C=US"
- name: Build release APK
run: |
cd carry-your-live/android
./gradlew assembleRelease
- name: Upload APK
uses: actions/upload-artifact@v4
with:
name: carry-your-live-release
path: carry-your-live/android/app/build/outputs/release/app-release.apk
-36
View File
@@ -1,36 +0,0 @@
name: Build Desktop (deb)
on:
push:
branches: [main]
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: 22
- name: Install JS dependencies
working-directory: carry-your-live
run: npm ci
- name: Export web bundle
working-directory: carry-your-live
run: npx expo export --platform web
- name: Build deb package
working-directory: carry-your-live
run: npx electron-builder --linux deb --config electron-builder.config.js
- name: Upload deb
uses: actions/upload-artifact@v4
with:
name: carry-your-live-desktop-deb
path: carry-your-live/desktop-build/*.deb
+84 -4
View File
@@ -1,15 +1,88 @@
name: release name: Build and Release APK
on: on:
push: push:
branches: branches: [main]
- main workflow_dispatch:
env:
ANDROID_HOME: ${{ runner.tool_cache }}/android-sdk
ANDROID_SDK_ROOT: ${{ runner.tool_cache }}/android-sdk
permissions: permissions:
contents: write contents: write
jobs: jobs:
build:
runs-on: ubuntu-latest
outputs:
apk-path: ${{ steps.upload-apk.outputs.path }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: 22
- name: Set up JDK
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 17
- name: Install Android SDK
run: |
if [ -x "$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" ]; then
echo "Android SDK already present in cache"
else
rm -rf "$ANDROID_HOME/cmdline-tools"
mkdir -p "$ANDROID_HOME/cmdline-tools"
wget -q https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip -O /tmp/cmdtools.zip
unzip -q /tmp/cmdtools.zip -d "$ANDROID_HOME/cmdline-tools"
mv "$ANDROID_HOME/cmdline-tools/cmdline-tools" "$ANDROID_HOME/cmdline-tools/latest"
fi
printf 'y\n' | "$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" --licenses > /dev/null
"$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" \
"platform-tools" \
"platforms;android-36" \
"build-tools;36.0.0" \
"ndk;27.1.12297006"
mkdir -p carry-your-live/android/app
echo "sdk.dir=$ANDROID_HOME" > carry-your-live/android/app/local.properties
- name: Install JS dependencies
working-directory: carry-your-live
run: |
for i in {1..3}; do
npm ci && break || sleep 10
done
- name: Generate debug keystore (signs the release APK)
run: |
mkdir -p carry-your-live/android/app
rm -f carry-your-live/android/app/debug.keystore
keytool -genkeypair -v \
-keystore carry-your-live/android/app/debug.keystore \
-alias androiddebugkey -storepass android -keypass android \
-keyalg RSA -keysize 2048 -validity 10000 \
-dname "CN=Android Debug,O=Android,C=US"
- name: Build release APK
run: |
cd carry-your-live/android
./gradlew assembleRelease
- name: Upload APK
id: upload-apk
uses: actions/upload-artifact@v4
with:
name: carry-your-live-release
path: carry-your-live/android/app/build/outputs/release/app-release.apk
release: release:
needs: build
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
@@ -17,6 +90,12 @@ jobs:
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Download APK artifact
uses: actions/download-artifact@v4
with:
name: carry-your-live-release
path: .
- name: Compute next version - name: Compute next version
id: version id: version
run: | run: |
@@ -53,4 +132,5 @@ jobs:
tag_name: ${{ steps.version.outputs.next }} tag_name: ${{ steps.version.outputs.next }}
name: ${{ steps.version.outputs.next }} name: ${{ steps.version.outputs.next }}
body: ${{ steps.changelog.outputs.body }} body: ${{ steps.changelog.outputs.body }}
generate_release_notes: false files: ./app-release.apk
generate_release_notes: false
+12 -1
View File
@@ -14,4 +14,15 @@ System architecture design, technical strategy, and scalability planning.
- System scalability - System scalability
- Performance benchmarks - Performance benchmarks
- Technical debt ratio - Technical debt ratio
- Architecture compliance - Architecture compliance
## Android Design Guidelines
- Material Design elevation patterns (0-12dp shadows)
- Tab bar height: 56dp (standard Android)
- Status bar: dark style for light backgrounds, light style for dark
- System back button: router.back() or goBack() with confirmation on home screen
- Touch ripple: opacity scaling on press (44dp touch targets, 8dp border radius)
- Safe area insets: account for bottom navigation bar (84px + system insets)
- Theme: primary accent color with complementary supporting colors
- Typography: Roboto-inspired font weights (600 for labels, 400/500 for body)
- Scaling: 8dp grid system for spacing and margins
+9
View File
@@ -2,6 +2,15 @@
A minimalist, offline-first task management app built with Expo, React Native, and WatermelonDB. Features three primary screens: To-Do List, Add New Task, and Calendar with bottom navigation. A minimalist, offline-first task management app built with Expo, React Native, and WatermelonDB. Features three primary screens: To-Do List, Add New Task, and Calendar with bottom navigation.
## 📝 Recent Fixes
- **ListItem component**: Fixed empty component - now properly renders list items with title, subtitle, icons, and swipe actions
- **Subtask drag-and-drop**: Added `parentSubtaskId` type support for proper subtask reparenting
- **Task drag sorting**: Added task drag-and-drop reordering functionality with visual drop indicators
- **iCal import**: Fixed import functionality - replaced invalid `splice()` with proper `create()` calls and fixed date type handling
- **Settings context**: Added `accentOpacity` and `setAccentOpacity` to the Settings context
- **Tag inheritance**: Subtasks no longer automatically inherit parent task tags when converting tasks to subtasks
## Features ## Features
### Core Functionality ### Core Functionality
Binary file not shown.
+1 -1
View File
@@ -10,7 +10,7 @@
# Specifies the JVM arguments used for the daemon process. # Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings. # The setting is particularly useful for tweaking memory settings.
# Default value: -Xmx512m -XX:MaxMetaspaceSize=256m # Default value: -Xmx512m -XX:MaxMetaspaceSize=256m
org.gradle.jvmargs=-Xmx1536m -XX:MaxMetaspaceSize=384m --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.io=ALL-UNNAMED org.gradle.jvmargs=-Xmx1536m -XX:MaxMetaspaceSize=1024m --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.io=ALL-UNNAMED
org.gradle.workers.max=2 org.gradle.workers.max=2
# When configured, Gradle will run in incubating parallel mode. # When configured, Gradle will run in incubating parallel mode.
+3
View File
@@ -1,4 +1,7 @@
pluginManagement { pluginManagement {
repositories {
gradlePluginPortal()
}
def reactNativeGradlePlugin = new File( def reactNativeGradlePlugin = new File(
providers.exec { providers.exec {
workingDir(rootDir) workingDir(rootDir)
+36 -28
View File
@@ -1,7 +1,7 @@
import { Tabs } from 'expo-router'; import { Tabs } from 'expo-router';
import React from 'react'; import React from 'react';
import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { View, StyleSheet, TouchableOpacity } from 'react-native'; import { View, StyleSheet, TouchableOpacity, Animated, } from 'react-native';
import { TabBarIcon } from '@/components/TabBarIcon'; import { TabBarIcon } from '@/components/TabBarIcon';
import { useSettings } from '@/theme'; import { useSettings } from '@/theme';
import { setLastVisitedTab } from '@/utils/tabHistory'; import { setLastVisitedTab } from '@/utils/tabHistory';
@@ -12,29 +12,30 @@ export default function TabLayout() {
return ( return (
<Tabs <Tabs
screenOptions={{ screenOptions={{
tabBarActiveTintColor: theme.accent, tabBarActiveTintColor: theme.accent,
tabBarInactiveTintColor: theme.textSecondary, tabBarInactiveTintColor: theme.textSecondary,
tabBarStyle: { tabBarStyle: {
backgroundColor: theme.tabBarBg, backgroundColor: theme.tabBarBg,
borderTopWidth: 0, borderTopWidth: 0,
height: 84 + insets.bottom, height: 56,
paddingBottom: insets.bottom, paddingBottom: insets.bottom,
paddingTop: 12, paddingTop: insets.top,
paddingHorizontal: 16, paddingHorizontal: 16,
elevation: 0, elevation: 4,
shadowColor: '#000', shadowColor: '#000',
shadowOffset: { width: 0, height: -4 }, shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.12, shadowOpacity: 0.1,
shadowRadius: 20, shadowRadius: 8,
}, },
tabBarItemStyle: { tabBarItemStyle: {
paddingVertical: 0, paddingVertical: 6,
paddingHorizontal: 12,
}, },
tabBarLabelStyle: { tabBarLabelStyle: {
fontSize: 11, fontSize: 12,
fontWeight: '700', fontWeight: '600',
marginTop: 4, marginTop: 2,
}, },
headerShown: false, headerShown: false,
sceneStyle: { backgroundColor: theme.background }, sceneStyle: { backgroundColor: theme.background },
@@ -48,13 +49,13 @@ export default function TabLayout() {
backgroundColor: theme.tabBarBg, backgroundColor: theme.tabBarBg,
paddingBottom: Math.max(props.insets.bottom, 8), paddingBottom: Math.max(props.insets.bottom, 8),
shadowColor: '#000', shadowColor: '#000',
shadowOffset: { width: 0, height: -4 }, shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.12, shadowOpacity: 0.1,
shadowRadius: 20, shadowRadius: 8,
elevation: 8, elevation: 4,
}, },
]} ]}
> >
{props.navigation.getState().routes.map((route) => { {props.navigation.getState().routes.map((route) => {
const focused = props.navigation.getState().index === props.navigation.getState().routes.indexOf(route); const focused = props.navigation.getState().index === props.navigation.getState().routes.indexOf(route);
return ( return (
@@ -62,13 +63,18 @@ export default function TabLayout() {
key={route.name} key={route.name}
style={[ style={[
styles.tabItem, styles.tabItem,
focused && styles.tabItemActive,
{ backgroundColor: focused ? theme.accentSoft : 'transparent' }, { backgroundColor: focused ? theme.accentSoft : 'transparent' },
]} ]}
onPress={() => props.navigation.navigate(route.name as never)} onPress={() => props.navigation.navigate(route.name as never)}
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel={route.name === 'index' ? 'ToDo' : route.name.charAt(0).toUpperCase() + route.name.slice(1)} accessibilityLabel={route.name === 'index' ? 'ToDo' : route.name.charAt(0).toUpperCase() + route.name.slice(1)}
accessibilityState={{ selected: focused }} accessibilityState={{ selected: focused }}
>
<Animated.View
style={[
styles.rippleContainer,
{ opacity: focused ? 1 : 0.5 },
]}
> >
<TabBarIcon <TabBarIcon
name={route.name === 'index' ? 'checklist' : route.name === 'calendar' ? 'calendar' : route.name === 'stats' ? 'stats' : 'gear'} name={route.name === 'index' ? 'checklist' : route.name === 'calendar' ? 'calendar' : route.name === 'stats' ? 'stats' : 'gear'}
@@ -76,6 +82,7 @@ export default function TabLayout() {
color={focused ? theme.accent : theme.textSecondary} color={focused ? theme.accent : theme.textSecondary}
size={24} size={24}
/> />
</Animated.View>
</TouchableOpacity> </TouchableOpacity>
); );
})} })}
@@ -103,22 +110,23 @@ const styles = StyleSheet.create({
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
justifyContent: 'space-around', justifyContent: 'space-around',
paddingTop: 10, paddingTop: 6,
paddingHorizontal: 16, paddingHorizontal: 16,
}, },
tabItem: { tabItem: {
flex: 1, flex: 1,
alignItems: 'center', alignItems: 'center',
paddingVertical: 8, paddingVertical: 6,
paddingHorizontal: 8, paddingHorizontal: 12,
borderRadius: 16, borderRadius: 8,
minWidth: 70, minWidth: 70,
}, },
tabItemActive: { rippleContainer: {
shadowColor: '#000', width: 44,
shadowOffset: { width: 0, height: 2 }, height: 44,
shadowOpacity: 0.1, borderRadius: 22,
shadowRadius: 8, alignItems: 'center',
elevation: 4, justifyContent: 'center',
position: 'relative',
}, },
}); });
+22 -2
View File
@@ -10,7 +10,7 @@ import { FriendsModal } from '@/components/FriendsModal';
import { LegalModal } from '@/components/LegalModal'; import { LegalModal } from '@/components/LegalModal';
import { ServerUrlModal } from '@/components/ServerUrlModal'; import { ServerUrlModal } from '@/components/ServerUrlModal';
import SyncStatus from '@/components/SyncStatus'; import SyncStatus from '@/components/SyncStatus';
import { useSettings, SORT_OPTIONS, REMINDER_OPTIONS, ACCENT_PRESETS, DEFAULT_ACCENT } from '@/theme'; import { useSettings, SORT_OPTIONS, REMINDER_OPTIONS, ACCENT_PRESETS, DEFAULT_ACCENT, DEFAULT_ACCENT_OPACITY } from '@/theme';
import { useCategories } from '@/hooks/useDatabase'; import { useCategories } from '@/hooks/useDatabase';
import { getAuthUser, getAuthToken } from '@/services/auth'; import { getAuthUser, getAuthToken } from '@/services/auth';
import { checkForUpdates, getCurrentAppVersion } from '@/services/updates'; import { checkForUpdates, getCurrentAppVersion } from '@/services/updates';
@@ -38,16 +38,19 @@ export default function SettingsScreen() {
const [draftAccentOpacity, setDraftAccentOpacity] = useState(accentOpacity); const [draftAccentOpacity, setDraftAccentOpacity] = useState(accentOpacity);
const [syncSubtitle, setSyncSubtitle] = useState('Checking...'); const [syncSubtitle, setSyncSubtitle] = useState('Checking...');
const [updateSubtitle, setUpdateSubtitle] = useState('Tap to check'); const [updateSubtitle, setUpdateSubtitle] = useState('Tap to check');
const [lastUpdateCheck, setLastUpdateCheck] = useState<string | null>(null);
const handleCheckUpdates = async () => { const handleCheckUpdates = async () => {
setUpdateSubtitle('Checking...'); setUpdateSubtitle('Checking...');
const update = await checkForUpdates(); const update = await checkForUpdates();
if (!update) { if (!update) {
setUpdateSubtitle('Up to date'); setUpdateSubtitle('Up to date');
setLastUpdateCheck(new Date().toLocaleString());
Alert.alert('Up to date', `You're running the latest version (${getCurrentAppVersion()}).`); Alert.alert('Up to date', `You're running the latest version (${getCurrentAppVersion()}).`);
return; return;
} }
setUpdateSubtitle(`${update.version} available`); setUpdateSubtitle(`${update.version} available`);
setLastUpdateCheck(new Date().toLocaleString());
const url = update.apkUrl ?? update.releaseUrl; const url = update.apkUrl ?? update.releaseUrl;
Alert.alert('Update available', `Version ${update.version} is available for download.`, [ Alert.alert('Update available', `Version ${update.version} is available for download.`, [
{ text: 'Later', style: 'cancel' }, { text: 'Later', style: 'cancel' },
@@ -60,6 +63,17 @@ export default function SettingsScreen() {
]); ]);
}; };
const checkForUpdatesOnLoad = async () => {
const update = await checkForUpdates();
if (!update) {
setUpdateSubtitle('Up to date');
setLastUpdateCheck(new Date().toLocaleString());
return;
}
setUpdateSubtitle(`${update.version} available`);
setLastUpdateCheck(new Date().toLocaleString());
};
const refreshSyncStatus = async () => { const refreshSyncStatus = async () => {
const token = await getAuthToken(); const token = await getAuthToken();
if (!token) { if (!token) {
@@ -84,6 +98,8 @@ export default function SettingsScreen() {
useFocusEffect( useFocusEffect(
useCallback(() => { useCallback(() => {
refreshSyncStatus();
checkForUpdatesOnLoad();
const sub = BackHandler.addEventListener('hardwareBackPress', () => { const sub = BackHandler.addEventListener('hardwareBackPress', () => {
const target = getLastVisitedTab(); const target = getLastVisitedTab();
if (target && target !== 'settings') { if (target && target !== 'settings') {
@@ -92,7 +108,7 @@ export default function SettingsScreen() {
return true; return true;
}); });
return () => sub.remove(); return () => sub.remove();
}, [router]) }, [refreshSyncStatus, router])
); );
const defaultCategory = categories.find((c) => c.id === defaultCategoryId); const defaultCategory = categories.find((c) => c.id === defaultCategoryId);
@@ -302,6 +318,10 @@ export default function SettingsScreen() {
onPress={handleCheckUpdates} onPress={handleCheckUpdates}
showChevron showChevron
/> />
<ListItem
title="Last checked"
subtitle={lastUpdateCheck ?? 'Never'}
/>
<ListItem <ListItem
title="Version" title="Version"
subtitle={getCurrentAppVersion()} subtitle={getCurrentAppVersion()}
+10 -2
View File
@@ -10,7 +10,6 @@ import { useEffect } from 'react';
import { requestQuickAddFocus } from '@/utils/quickAddFocus'; import { requestQuickAddFocus } from '@/utils/quickAddFocus';
import { registerForPushNotificationsAsync, setupNotificationChannels, addNotificationResponseReceivedListener, removeNotificationSubscription } from '@/services/pushNotifications'; import { registerForPushNotificationsAsync, setupNotificationChannels, addNotificationResponseReceivedListener, removeNotificationSubscription } from '@/services/pushNotifications';
import * as Notifications from 'expo-notifications'; import * as Notifications from 'expo-notifications';
import { Platform } from 'react-native';
Notifications.setNotificationHandler({ Notifications.setNotificationHandler({
handleNotification: async () => ({ handleNotification: async () => ({
@@ -57,15 +56,24 @@ function RootNavigator() {
Linking.getInitialURL().then(handleUrl).catch(() => {}); Linking.getInitialURL().then(handleUrl).catch(() => {});
const subscription = Linking.addEventListener('url', ({ url }) => handleUrl(url)); const subscription = Linking.addEventListener('url', ({ url }) => handleUrl(url));
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
router.back();
}
};
window.addEventListener('keydown', handleKeyDown);
return () => { return () => {
subscription.remove(); subscription.remove();
removeNotificationSubscription(responseListener); removeNotificationSubscription(responseListener);
window.removeEventListener('keydown', handleKeyDown);
}; };
}, [router]); }, [router]);
return ( return (
<GestureHandlerRootView style={{ flex: 1 }}> <GestureHandlerRootView style={{ flex: 1 }}>
<StatusBar style="light" /> <StatusBar style="dark" />
<Stack screenOptions={{ headerShown: false }}> <Stack screenOptions={{ headerShown: false }}>
<Stack.Screen name="(tabs)" /> <Stack.Screen name="(tabs)" />
<Stack.Screen name="add-task" /> <Stack.Screen name="add-task" />
@@ -161,7 +161,7 @@ export function CategoryEditorModal({
</TouchableOpacity> </TouchableOpacity>
))} ))}
</View> </View>
<ColorWheel color={color} onChange={setColor} /> <ColorWheel color={color} opacity={1} onChange={setColor} />
<View style={styles.actions}> <View style={styles.actions}>
{category && ( {category && (
@@ -67,6 +67,7 @@ export function TaskList({ categoryIds = [], showCompleted = false, onSelectionC
const subtaskRefs = useRef<Map<string, { ref: View; parentTaskId: string }>>(new Map()); const subtaskRefs = useRef<Map<string, { ref: View; parentTaskId: string }>>(new Map());
const dragStateRef = useRef<{ taskId: string; positions: Record<string, { top: number; bottom: number }>; subPositions: Record<string, { top: number; bottom: number }> } | null>(null); const dragStateRef = useRef<{ taskId: string; positions: Record<string, { top: number; bottom: number }>; subPositions: Record<string, { top: number; bottom: number }> } | null>(null);
const subtaskDragRef = useRef<{ subtaskId: string; parentTaskId: string; parentSubtaskId: string | null; positions: Record<string, { top: number; bottom: number }>; subPositions: Record<string, { top: number; bottom: number }> } | null>(null); const subtaskDragRef = useRef<{ subtaskId: string; parentTaskId: string; parentSubtaskId: string | null; positions: Record<string, { top: number; bottom: number }>; subPositions: Record<string, { top: number; bottom: number }> } | null>(null);
const taskDragRef = useRef<{ taskId: string; positions: Record<string, { top: number; bottom: number }> } | null>(null);
const { const {
modals, modals,
openTaskDelete, openTaskDelete,
@@ -237,6 +238,22 @@ export function TaskList({ categoryIds = [], showCompleted = false, onSelectionC
return { positions, subPositions }; return { positions, subPositions };
}, [measureItems]); }, [measureItems]);
const findHoverTask = useCallback((absoluteY: number, draggedId: string, positions: Record<string, { top: number; bottom: number }>) => {
const entries = Object.entries(positions);
for (let i = entries.length - 1; i >= 0; i--) {
const [id, p] = entries[i];
if (id === draggedId) continue;
if (absoluteY >= p.top && absoluteY <= p.bottom) {
return id;
}
}
return null;
}, []);
const updateHoverTaskTarget = useCallback((target: string | null) => {
setHoverTaskId(target ?? null);
}, []);
const findHoverTarget = useCallback((absoluteY: number, draggedId: string, positions: Record<string, { top: number; bottom: number }>, subPositions: Record<string, { top: number; bottom: number }>) => { const findHoverTarget = useCallback((absoluteY: number, draggedId: string, positions: Record<string, { top: number; bottom: number }>, subPositions: Record<string, { top: number; bottom: number }>) => {
const subEntries = Object.entries(subPositions); const subEntries = Object.entries(subPositions);
for (let i = subEntries.length - 1; i >= 0; i--) { for (let i = subEntries.length - 1; i >= 0; i--) {
@@ -381,6 +398,49 @@ const handleSubtaskDragEnd = useCallback(async (absoluteY: number) => {
} }
}, [findHoverTarget, moveSubtaskToTask]); }, [findHoverTarget, moveSubtaskToTask]);
const handleTaskDragStart = useCallback(async (taskId: string) => {
const positions = await measureItems() as Record<string, { top: number; bottom: number }>;
taskDragRef.current = { taskId, positions };
}, [measureItems]);
const handleTaskDragUpdate = useCallback((absoluteY: number) => {
const state = taskDragRef.current;
if (!state) return;
const target = findHoverTask(absoluteY, state.taskId, state.positions);
updateHoverTaskTarget(target);
if (target) {
setDropIndicator({ targetId: target, position: 'below' });
} else {
const lastItem = Object.values(state.positions).reduce((max, p) => p.bottom > max.bottom ? p : max, { bottom: 0 });
if (absoluteY > lastItem.bottom) {
setDropIndicator({ targetId: null, position: 'below' });
} else {
setDropIndicator(null);
}
}
}, [findHoverTask, updateHoverTaskTarget]);
const handleTaskDragEnd = useCallback(async (absoluteY: number) => {
const state = taskDragRef.current;
taskDragRef.current = null;
setHoverTaskId(null);
setDropIndicator(null);
if (!state) return;
const target = findHoverTask(absoluteY, state.taskId, state.positions);
if (target === state.taskId) return;
// Set drop indicator based on target position
if (target) {
setDropIndicator({ targetId: target, position: 'below' });
} else {
const lastItem = Object.values(state.positions).reduce((max, p) => p.bottom > max.bottom ? p : max, { bottom: 0 });
if (absoluteY > lastItem.bottom) {
setDropIndicator({ targetId: null, position: 'below' });
}
}
}, [findHoverTask]);
const renderItem = useCallback( const renderItem = useCallback(
({ item, index }: { item: Task; index: number }) => { ({ item, index }: { item: Task; index: number }) => {
const isExpanded = expandedTasks.has(item.id); const isExpanded = expandedTasks.has(item.id);
@@ -220,5 +220,9 @@ export const migrations = schemaMigrations({
}), }),
], ],
}, },
{
toVersion: 21,
steps: [],
},
], ],
}); });
+1 -2
View File
@@ -11,7 +11,6 @@ import RepeatProfile from '@/models/RepeatProfile';
import Friendship from '@/models/Friendship'; import Friendship from '@/models/Friendship';
import { TombstoneEntity } from '@/models/Tombstone'; import { TombstoneEntity } from '@/models/Tombstone';
import { fetchPendingTombstones, removeTombstonesInBatch } from './tombstones'; import { fetchPendingTombstones, removeTombstonesInBatch } from './tombstones';
import { useOnline } from '@/hooks/useOnline';
const LAST_PULLED_AT_KEY = 'sync:lastPulledAt'; const LAST_PULLED_AT_KEY = 'sync:lastPulledAt';
const LAST_PUSHED_AT_KEY = 'sync:lastPushedAt'; const LAST_PUSHED_AT_KEY = 'sync:lastPushedAt';
@@ -96,7 +95,7 @@ export async function runSync(): Promise<SyncResult> {
} }
// If offline, mark timestamps and return early - changes will sync on next foreground launch // If offline, mark timestamps and return early - changes will sync on next foreground launch
const isCurrentlyOnline = await useOnline(); const isCurrentlyOnline = navigator.onLine;
if (!isCurrentlyOnline) { if (!isCurrentlyOnline) {
const now = Date.now(); const now = Date.now();
await setLastPulledAt(now); await setLastPulledAt(now);
+28 -42
View File
@@ -2,6 +2,7 @@ import { useDatabase } from './useDatabase';
import { Q } from '@nozbe/watermelondb'; import { Q } from '@nozbe/watermelondb';
import { useEffect, useState, useCallback } from 'react'; import { useEffect, useState, useCallback } from 'react';
import Task from '../models/Task'; import Task from '../models/Task';
import { Priority, Repeat, Reminder } from '@/types';
import parseICal from '../utils/icalParser'; import parseICal from '../utils/icalParser';
import { startOfMonth, endOfMonth, addMonths, format } from 'date-fns'; import { startOfMonth, endOfMonth, addMonths, format } from 'date-fns';
@@ -138,49 +139,34 @@ export function useICalImport() {
} }
const taskData = events.map(mapICalEventToTask); const taskData = events.map(mapICalEventToTask);
const now = Date.now();
// Insert tasks into WatermelonDB // Insert tasks into WatermelonDB
const subscription = collections.tasks await Promise.all(
.query() taskData.map((td) =>
.splice( collections.tasks.create((t) => {
0, t.title = td.title;
0, t.description = td.description;
...taskData.map((td) => ({ t.categoryId = td.categoryId;
title: td.title, t.priority = td.priority as Priority;
description: td.description, t.completed = td.completed;
category_id: td.categoryId, t.completedAt = td.completedAt;
tags: td.tags, t.dueDate = td.dueDate;
priority: td.priority, t.dueTime = td.dueTime;
completed: td.completed, t.endTime = td.endTime;
completed_at: td.completedAt, t.allDay = td.allDay;
due_date: td.dueDate, t.repeat = td.repeat as Repeat;
due_time: td.dueTime, t.repeatInterval = td.repeatInterval;
end_time: td.endTime, t.repeatDays = td.repeatDays;
all_day: td.allDay, t.seriesId = td.seriesId;
repeat: td.repeat, t.reminder = td.reminder as Reminder;
repeat_interval: td.repeatInterval, t.reminders = td.reminders;
repeat_days: td.repeatDays, t.assigneeId = null;
color: '#9E9E9E', t.createdAt = new Date(now);
series_id: td.seriesId, t.updatedAt = new Date(now);
order: 0, })
reminder: td.reminder, )
reminders: td.reminders, );
assignee_id: null,
created_at: now,
updated_at: now,
}))
);
// Observe the changes
subscription.subscribe({
next: (result) => {
// Tasks have been imported
},
error: (err) => {
console.error('Import error:', err);
setError(err.message);
},
});
setLoading(false); setLoading(false);
} catch (err: any) { } catch (err: any) {
@@ -188,7 +174,7 @@ export function useICalImport() {
setError(err.message || 'Import failed'); setError(err.message || 'Import failed');
setLoading(false); setLoading(false);
} }
}, [collections]); }, []);
return { importICal, loading, error }; return { importICal, loading, error };
} }
+4
View File
@@ -154,6 +154,8 @@ interface SettingsContextType {
setApiUrl: (value: string) => void; setApiUrl: (value: string) => void;
accentColor: string; accentColor: string;
setAccentColor: (value: string) => void; setAccentColor: (value: string) => void;
accentOpacity: number;
setAccentOpacity: (value: number) => void;
todoAheadDays: number; todoAheadDays: number;
setTodoAheadDays: (value: number) => void; setTodoAheadDays: (value: number) => void;
showCompleted: boolean; showCompleted: boolean;
@@ -243,6 +245,8 @@ export function SettingsProvider({ children }: { children: ReactNode }) {
setApiUrl, setApiUrl,
accentColor, accentColor,
setAccentColor, setAccentColor,
accentOpacity,
setAccentOpacity,
todoAheadDays, todoAheadDays,
setTodoAheadDays, setTodoAheadDays,
showCompleted, showCompleted,
-2
View File
@@ -1,2 +0,0 @@
tech08mag
7BXtaqyvwVB15W
+35 -2
View File
@@ -1,4 +1,37 @@
{ {
"compilerOptions": {}, "compilerOptions": {
"extends": "expo/tsconfig.base" "allowJs": true,
"esModuleInterop": true,
"jsx": "react-jsx",
"lib": ["DOM", "ESNext"],
"module": "preserve",
"moduleDetection": "force",
"moduleResolution": "bundler",
"customConditions": ["react-native"],
"noEmit": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"target": "ESNext",
"baseUrl": ".",
"deprecationDiagnostics": false,
"paths": {
"@/*": ["carry-your-live/src/*"],
"@/components/*": ["carry-your-live/src/components/*"],
"@/hooks/*": ["carry-your-live/src/hooks/*"],
"@/database/*": ["carry-your-live/src/database/*"],
"@/models/*": ["carry-your-live/src/models/*"],
"@/constants/*": ["carry-your-live/src/constants/*"],
"@/types/*": ["carry-your-live/src/types/*"],
"@/utils/*": ["carry-your-live/src/utils/*"]
}
},
"extends": "expo/tsconfig.base",
"exclude": [
"${configDir}/node_modules",
"${configDir}/babel.config.js",
"${configDir}/metro.config.js",
"${configDir}/jest.config.js",
"${configDir}/android",
"${configDir}/ios"
]
} }