Time units, some fixes
This commit is contained in:
parent
db495f6f1e
commit
be2335e3bc
@ -92,8 +92,8 @@ android {
|
|||||||
applicationId 'com.xaviscript.taskeep'
|
applicationId 'com.xaviscript.taskeep'
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 1
|
versionCode 4
|
||||||
versionName "1.0.0"
|
versionName "1.1.0"
|
||||||
}
|
}
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
release {
|
release {
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 49 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 72 KiB |
@ -1 +1,3 @@
|
|||||||
<resources/>
|
<resources>
|
||||||
|
<color name="splashscreen_background">#232323</color>
|
||||||
|
</resources>
|
||||||
@ -58,10 +58,10 @@ expo.useLegacyPackaging=false
|
|||||||
android.packagingOptions.pickFirsts=**/libc++_shared.so
|
android.packagingOptions.pickFirsts=**/libc++_shared.so
|
||||||
expo.sqlite.enableFTS=false
|
expo.sqlite.enableFTS=false
|
||||||
expo.sqlite.useSQLCipher=false
|
expo.sqlite.useSQLCipher=false
|
||||||
# Whether the app is configured to use edge-to-edge via the app config or `react-native-edge-to-edge` plugin
|
|
||||||
expo.edgeToEdgeEnabled=true
|
|
||||||
|
|
||||||
MYAPP_UPLOAD_STORE_FILE=taskeep.keystore
|
MYAPP_UPLOAD_STORE_FILE=taskeep.keystore
|
||||||
MYAPP_UPLOAD_KEY_ALIAS=taskeep
|
MYAPP_UPLOAD_KEY_ALIAS=taskeep
|
||||||
MYAPP_UPLOAD_STORE_PASSWORD=123?_Tk_?123
|
MYAPP_UPLOAD_STORE_PASSWORD=123?_Tk_?123
|
||||||
MYAPP_UPLOAD_KEY_PASSWORD=123?_Tk_?123
|
MYAPP_UPLOAD_KEY_PASSWORD=123?_Tk_?123
|
||||||
|
# Whether the app is configured to use edge-to-edge via the app config or `react-native-edge-to-edge` plugin
|
||||||
|
expo.edgeToEdgeEnabled=true
|
||||||
11
app.json
11
app.json
@ -3,7 +3,7 @@
|
|||||||
"name": "Taskeep",
|
"name": "Taskeep",
|
||||||
"main": "index.tsx",
|
"main": "index.tsx",
|
||||||
"slug": "taskeep",
|
"slug": "taskeep",
|
||||||
"version": "1.0.0",
|
"version": "1.1.0",
|
||||||
"orientation": "portrait",
|
"orientation": "portrait",
|
||||||
"icon": "./assets/images/icon.png",
|
"icon": "./assets/images/icon.png",
|
||||||
"scheme": "taskeep",
|
"scheme": "taskeep",
|
||||||
@ -18,7 +18,8 @@
|
|||||||
"backgroundColor": "#ffffff"
|
"backgroundColor": "#ffffff"
|
||||||
},
|
},
|
||||||
"edgeToEdgeEnabled": true,
|
"edgeToEdgeEnabled": true,
|
||||||
"package": "com.xaviscript.taskeep"
|
"package": "com.xaviscript.taskeep",
|
||||||
|
"versionCode": 4
|
||||||
},
|
},
|
||||||
"web": {
|
"web": {
|
||||||
"bundler": "metro",
|
"bundler": "metro",
|
||||||
@ -33,7 +34,11 @@
|
|||||||
"image": "./assets/images/splash-icon.png",
|
"image": "./assets/images/splash-icon.png",
|
||||||
"imageWidth": 200,
|
"imageWidth": 200,
|
||||||
"resizeMode": "contain",
|
"resizeMode": "contain",
|
||||||
"backgroundColor": "#ffffff"
|
"backgroundColor": "#ffffff",
|
||||||
|
"dark": {
|
||||||
|
"image": "./assets/images/splash-icon.png",
|
||||||
|
"backgroundColor": "#232323"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import Ionicons from '@expo/vector-icons/Ionicons';
|
|||||||
import { useFocusEffect } from 'expo-router';
|
import { useFocusEffect } from 'expo-router';
|
||||||
import { navigate } from 'expo-router/build/global-state/routing';
|
import { navigate } from 'expo-router/build/global-state/routing';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { PermissionsAndroid, Platform } from 'react-native';
|
import { PermissionsAndroid, Platform } from 'react-native';
|
||||||
import { TaskItem } from '../../components/taskItem';
|
import { TaskItem } from '../../components/taskItem';
|
||||||
import { ThemedText } from '../../components/ThemedText';
|
import { ThemedText } from '../../components/ThemedText';
|
||||||
@ -33,6 +34,7 @@ export default function HomeScreen() {
|
|||||||
const [tasks, setTasks] = useState<Task[]>([]);
|
const [tasks, setTasks] = useState<Task[]>([]);
|
||||||
const [categories, setCategories] = useState<Category[]>([]);
|
const [categories, setCategories] = useState<Category[]>([]);
|
||||||
const [ready, setReady] = useState(false);
|
const [ready, setReady] = useState(false);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
useEffect(() => { // Initial data fetch
|
useEffect(() => { // Initial data fetch
|
||||||
(async () => {
|
(async () => {
|
||||||
@ -68,30 +70,13 @@ export default function HomeScreen() {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
async function schedulePushNotification() {
|
|
||||||
if (Platform.OS === 'web' || initiated.current) return;
|
|
||||||
initiated.current = true;
|
|
||||||
await Notifications.cancelAllScheduledNotificationsAsync();
|
|
||||||
await Notifications.dismissAllNotificationsAsync();
|
|
||||||
await Notifications.scheduleNotificationAsync({
|
|
||||||
content: {
|
|
||||||
title: `${t("remember_notification_title")} 🌠`,
|
|
||||||
body: t("remember_notification_body"),
|
|
||||||
},
|
|
||||||
trigger: {
|
|
||||||
hour: 22,
|
|
||||||
minute: 0,
|
|
||||||
repeats: true
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const RefreshTasks = async () => {
|
const RefreshTasks = async () => {
|
||||||
try {
|
try {
|
||||||
const fetchedTasks = await taskRepository.findAll();
|
const fetchedTasks = await taskRepository.findAll();
|
||||||
fetchedTasks.sort((a, b) => {
|
fetchedTasks.sort((a, b) => {
|
||||||
return (a.daysToRedo! * 24 * 3600 * 1000 - (Date.now() - a.lastDone!)) - ((b.daysToRedo! * 24 * 3600 * 1000) - (Date.now() - b.lastDone!));
|
return (a.daysToRedo! * 24 * 3600 * 1000 - (Date.now() - a.lastDone!)) - ((b.daysToRedo! * 24 * 3600 * 1000) - (Date.now() - b.lastDone!));
|
||||||
});
|
})
|
||||||
console.log("Tasks fetched:", fetchedTasks);
|
console.log("Tasks fetched:", fetchedTasks);
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -44,11 +44,11 @@ export default function CategoryForm() {
|
|||||||
<EmojiPopup onEmojiSelected={emoji => setNewCategory({ ...newCategory, icon: emoji })} >
|
<EmojiPopup onEmojiSelected={emoji => setNewCategory({ ...newCategory, icon: emoji })} >
|
||||||
<ThemedView style={styles.addIconView}>
|
<ThemedView style={styles.addIconView}>
|
||||||
{newCategory.icon != "" ? <ThemedText>{newCategory.icon}</ThemedText> : <Ionicons name={newCategory.icon || "happy-outline"} size={24} color="#ccc" />}
|
{newCategory.icon != "" ? <ThemedText>{newCategory.icon}</ThemedText> : <Ionicons name={newCategory.icon || "happy-outline"} size={24} color="#ccc" />}
|
||||||
<ThemedText>{newCategory.icon != "" ? "Change" : "Select"} icon</ThemedText>
|
<ThemedText>{newCategory.icon != "" ? "Change" : "Select"} icon *</ThemedText>
|
||||||
</ThemedView>
|
</ThemedView>
|
||||||
</EmojiPopup>
|
</EmojiPopup>
|
||||||
<TextInput
|
<TextInput
|
||||||
placeholder="Task title *"
|
placeholder="Category title *"
|
||||||
style={styles.inputText}
|
style={styles.inputText}
|
||||||
placeholderTextColor="#333"
|
placeholderTextColor="#333"
|
||||||
value={newCategory.title}
|
value={newCategory.title}
|
||||||
|
|||||||
@ -14,12 +14,42 @@ import { SQLiteDataService } from '../../services/data/sqliteDataService';
|
|||||||
|
|
||||||
const taskRepository = new TaskRepository(new SQLiteDataService<Task>(TaskQuery));
|
const taskRepository = new TaskRepository(new SQLiteDataService<Task>(TaskQuery));
|
||||||
const categoryRepository = new CategoryRepository(new SQLiteDataService<Category>(CategoryQuery));
|
const categoryRepository = new CategoryRepository(new SQLiteDataService<Category>(CategoryQuery));
|
||||||
|
const timeUnits = [
|
||||||
|
{ label: 'Minutes', value: 'minutes' },
|
||||||
|
{ label: 'Hours', value: 'hours' },
|
||||||
|
{ label: 'Days', value: 'days' },
|
||||||
|
{ label: 'Weeks', value: 'weeks' },
|
||||||
|
{ label: 'Months', value: 'months' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const timeUnitToDays = (num: number, unit: string): number => {
|
||||||
|
let result = 0;
|
||||||
|
switch (unit) { // convert to days
|
||||||
|
case 'minutes':
|
||||||
|
result = num / 1440; // Convert minutes to days
|
||||||
|
break;
|
||||||
|
case 'hours':
|
||||||
|
result = num / 24; // Convert hours to days
|
||||||
|
break;
|
||||||
|
case 'days':
|
||||||
|
result = num; // Convert days to seconds
|
||||||
|
break;
|
||||||
|
case 'weeks':
|
||||||
|
result = num * 7; // Convert weeks to days
|
||||||
|
break;
|
||||||
|
case 'months':
|
||||||
|
result = num * 30; // Convert months to days (approximation)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
export default function TasksForm() {
|
export default function TasksForm() {
|
||||||
|
|
||||||
const { task: taskJson } = useLocalSearchParams();
|
const { task: taskJson } = useLocalSearchParams();
|
||||||
const task: Task = taskJson ? JSON.parse(taskJson as string) : null;
|
const task: Task = taskJson ? JSON.parse(taskJson as string) : null;
|
||||||
const [categories, setCategories] = useState<Category[]>([]);
|
const [categories, setCategories] = useState<Category[]>([]);
|
||||||
|
const [selectedTimeUnit, setSelectedTimeUnit] = useState<string>("days");
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
|
||||||
@ -29,13 +59,35 @@ export default function TasksForm() {
|
|||||||
category: task?.category || -1,
|
category: task?.category || -1,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const adaptDaysToGoToUnit = () => {
|
||||||
|
if (!task) return;
|
||||||
|
if (task.daysToRedo! <= 1000 * 3600) {
|
||||||
|
setSelectedTimeUnit("minutes");
|
||||||
|
setNewTask({ ...newTask, daysToRedo: task.daysToRedo! * 60 * 24 });
|
||||||
|
}
|
||||||
|
else if (task.daysToRedo! <= 1000 * 3600 * 24) {
|
||||||
|
setSelectedTimeUnit("hours");
|
||||||
|
setNewTask({ ...newTask, daysToRedo: task.daysToRedo! * 24 });
|
||||||
|
} else if (task.daysToRedo! <= 1000 * 3600 * 24 * 7) {
|
||||||
|
setSelectedTimeUnit("days");
|
||||||
|
setNewTask({ ...newTask, daysToRedo: task.daysToRedo! });
|
||||||
|
} else if (task.daysToRedo! <= 1000 * 3600 * 24 * 30) {
|
||||||
|
setSelectedTimeUnit("weeks");
|
||||||
|
setNewTask({ ...newTask, daysToRedo: task.daysToRedo! / 7 });
|
||||||
|
} else {
|
||||||
|
setSelectedTimeUnit("months");
|
||||||
|
setNewTask({ ...newTask, daysToRedo: task.daysToRedo! / 30 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
useFocusEffect(React.useCallback(() => {
|
useFocusEffect(React.useCallback(() => {
|
||||||
categoryRepository.findAll().then((categories) => {
|
categoryRepository.findAll().then((categories) => {
|
||||||
categories.push(new Category("_create new_", "➕", 0));
|
categories.push(new Category("Create new category", "➕", 0));
|
||||||
setCategories(categories);
|
setCategories(categories);
|
||||||
if (newTask.category === 0) {
|
if (newTask.category === 0) {
|
||||||
setNewTask({ ...newTask, category: -1 });
|
setNewTask({ ...newTask, category: -1 });
|
||||||
}
|
}
|
||||||
|
adaptDaysToGoToUnit();
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
console.error("Error fetching categories:", error);
|
console.error("Error fetching categories:", error);
|
||||||
});
|
});
|
||||||
@ -65,14 +117,47 @@ export default function TasksForm() {
|
|||||||
value={newTask.title}
|
value={newTask.title}
|
||||||
onChange={(e) => setNewTask({ ...newTask, title: e.nativeEvent.text })}
|
onChange={(e) => setNewTask({ ...newTask, title: e.nativeEvent.text })}
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<View style={{ flexDirection: 'row', gap: 8, height: 60 }}>
|
||||||
placeholder="Days after which task is due *"
|
<TextInput
|
||||||
style={styles.inputText}
|
placeholder={`${timeUnits.find(unit => unit.value === selectedTimeUnit)?.label} after which task is due *`}
|
||||||
placeholderTextColor="#333"
|
style={styles.inputText}
|
||||||
keyboardType='numeric'
|
placeholderTextColor="#333"
|
||||||
value={isNaN(newTask.daysToRedo ?? NaN) ? undefined : newTask.daysToRedo?.toString()}
|
keyboardType='numeric'
|
||||||
onChange={(e) => setNewTask({ ...newTask, daysToRedo: e.nativeEvent.text.length > 0 ? parseInt(e.nativeEvent.text) : NaN })}
|
value={isNaN(newTask.daysToRedo ?? NaN) ? undefined : newTask.daysToRedo?.toString()}
|
||||||
/>
|
onChange={(e) => setNewTask({ ...newTask, daysToRedo: e.nativeEvent.text.length > 0 ? parseInt(e.nativeEvent.text) : NaN })}
|
||||||
|
/>
|
||||||
|
<Dropdown
|
||||||
|
options={timeUnits}
|
||||||
|
onValueChange={(value) => setSelectedTimeUnit(value?.toString() ?? "")}
|
||||||
|
selectedValue={selectedTimeUnit}
|
||||||
|
maxSelectableItems={1}
|
||||||
|
checkboxControls={{
|
||||||
|
checkboxLabelStyle: { fontSize: 20, paddingVertical: 10 },
|
||||||
|
checkboxStyle: {
|
||||||
|
backgroundColor: '#e744a9ff',
|
||||||
|
borderColor: 'transparent',
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
dropdownIconStyle={{
|
||||||
|
alignSelf: 'flex-start',
|
||||||
|
margin: 0,
|
||||||
|
padding: 0,
|
||||||
|
marginTop: -30
|
||||||
|
}}
|
||||||
|
dropdownContainerStyle={
|
||||||
|
{
|
||||||
|
flex: 1,
|
||||||
|
marginTop: -2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dropdownStyle={{
|
||||||
|
borderColor: '#ccc',
|
||||||
|
borderWidth: 2,
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
placeholder="Select category *"
|
placeholder="Select category *"
|
||||||
options={
|
options={
|
||||||
@ -103,9 +188,15 @@ export default function TasksForm() {
|
|||||||
padding: 0,
|
padding: 0,
|
||||||
marginTop: -30
|
marginTop: -30
|
||||||
}}
|
}}
|
||||||
|
dropdownStyle={{
|
||||||
|
borderColor: '#ccc',
|
||||||
|
borderWidth: 2
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</ThemedView>
|
</ThemedView>
|
||||||
<Pressable style={styles.addButton} onPress={() => {
|
<Pressable style={styles.addButton} onPress={() => {
|
||||||
|
|
||||||
|
newTask.daysToRedo = timeUnitToDays(newTask.daysToRedo, selectedTimeUnit);
|
||||||
if (newTask.title.length > 0 && newTask.daysToRedo > 0 && newTask.category > -1) {
|
if (newTask.title.length > 0 && newTask.daysToRedo > 0 && newTask.category > -1) {
|
||||||
if (task && task.id) {
|
if (task && task.id) {
|
||||||
// Update existing task
|
// Update existing task
|
||||||
@ -167,8 +258,9 @@ const styles = StyleSheet.create({
|
|||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
},
|
},
|
||||||
inputText: {
|
inputText: {
|
||||||
|
flex: 1,
|
||||||
backgroundColor: '#fff',
|
backgroundColor: '#fff',
|
||||||
height: 50,
|
height: 60,
|
||||||
color: '#333333',
|
color: '#333333',
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
padding: 10,
|
padding: 10,
|
||||||
|
|||||||
@ -5,16 +5,33 @@ import { Category } from '../../models/category';
|
|||||||
import { Task } from '../../models/task';
|
import { Task } from '../../models/task';
|
||||||
import { GetTaskColor } from '../../utils/colors';
|
import { GetTaskColor } from '../../utils/colors';
|
||||||
|
|
||||||
|
const adaptDaysToGoToUnit = (t: Task) => {
|
||||||
|
const timeLeft = t.lastDone! + (t.daysToRedo! * 24 * 60 * 60 * 1000) - Date.now();
|
||||||
|
if (timeLeft < 0) return 0; // Task is overdue
|
||||||
|
else if (timeLeft < 3 * 60 * 60 * 1000) return Math.ceil(timeLeft / (60 * 1000)) + " minutes";
|
||||||
|
else if (timeLeft < 24 * 60 * 60 * 1000) return Math.ceil(timeLeft / (3600 * 1000)) + " hours";
|
||||||
|
else if (timeLeft < 7 * 24 * 60 * 60 * 1000) return Math.ceil(timeLeft / (24 * 3600 * 1000)) + " days";
|
||||||
|
else if (timeLeft < 30 * 24 * 60 * 60 * 1000) return Math.ceil(timeLeft / (7 * 24 * 3600 * 1000)) + " weeks";
|
||||||
|
else return Math.ceil(timeLeft / (30 * 24 * 3600 * 1000)) + " months";
|
||||||
|
};
|
||||||
|
|
||||||
export function HelloWidget({ tasks, categories }: { tasks: Task[], categories?: Category[] }) {
|
export function HelloWidget({ tasks, categories }: { tasks: Task[], categories?: Category[] }) {
|
||||||
|
|
||||||
const demoTasks: Task[] = [
|
const demoTasks: Task[] = [
|
||||||
new Task('Task 1', 3, 1, new Date().getTime()),
|
new Task('Task 1', 1, 1, new Date().getTime() - 1000 * 60 * 60 * 24 * 0, 5), // 2 days ago
|
||||||
new Task('Task 2', 5, 1, new Date().getTime()),
|
new Task('Task 2', 2, 1, new Date().getTime() - 1000 * 60 * 60 * 24), // 1 day ago
|
||||||
new Task('Task 2', 5, 1, new Date().getTime()),
|
new Task('Task 3', 3, 1, new Date().getTime() - 1000 * 60 * 60 * 24 * 2), // 2 days ago
|
||||||
new Task('Task 2', 5, 1, new Date().getTime()),
|
new Task('Task 4', 4, 1, new Date().getTime() - 1000 * 60 * 60 * 24 * 3), // 3 days ago
|
||||||
new Task('Task 2', 5, 1, new Date().getTime())
|
new Task('Task 5', 5, 1, new Date().getTime() - 1000 * 60 * 60 * 24 * 4), // 4 days ago
|
||||||
|
new Task('Task 6', 6, 1, new Date().getTime() - 1000 * 60 * 60 * 24 * 10), // 4 days ago
|
||||||
];
|
];
|
||||||
|
|
||||||
|
tasks = (tasks ?? demoTasks)?.sort((a, b) => {
|
||||||
|
const aExpiration = (Date.now() - a.lastDone!) / (a.daysToRedo! * 24 * 60 * 60 * 1000);
|
||||||
|
const bExpiration = (Date.now() - b.lastDone!) / (b.daysToRedo! * 24 * 60 * 60 * 1000);
|
||||||
|
return aExpiration - bExpiration;
|
||||||
|
}).reverse();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FlexWidget
|
<FlexWidget
|
||||||
style={{
|
style={{
|
||||||
@ -50,61 +67,75 @@ export function HelloWidget({ tasks, categories }: { tasks: Task[], categories?:
|
|||||||
height: 'match_parent',
|
height: 'match_parent',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{(tasks ?? demoTasks).map((task, index) => (
|
{tasks.map((task, index) => {
|
||||||
<FlexWidget
|
|
||||||
key={Date.now() + "-" + Math.random() + "-" + index + "-" + task.id}
|
const category = categories?.find(c => c.id === task.category);
|
||||||
style={{
|
const timeLeft = adaptDaysToGoToUnit(task)
|
||||||
flexDirection: 'row',
|
const circleDegree = parseInt(timeLeft.toString()) > 0 ? ((task.daysToRedo! - (((Date.now() - task.lastDone!) / (24 * 3600 * 1000)))) / task.daysToRedo!) * 360 : 0;
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'space-between',
|
return (
|
||||||
width: 'match_parent',
|
|
||||||
paddingVertical: 8
|
|
||||||
}}>
|
|
||||||
<FlexWidget
|
<FlexWidget
|
||||||
|
key={Date.now() + "-" + Math.random() + "-" + index + "-" + task.id}
|
||||||
style={{
|
style={{
|
||||||
width: 'match_parent',
|
|
||||||
borderLeftWidth: 20,
|
|
||||||
borderLeftColor: GetTaskColor(task) as ColorProp,
|
|
||||||
alignItems: 'center',
|
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
paddingVertical: 16,
|
width: 'match_parent',
|
||||||
paddingHorizontal: 16,
|
paddingVertical: 8
|
||||||
borderRadius: 8,
|
}}>
|
||||||
backgroundColor: '#333333',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FlexWidget
|
<FlexWidget
|
||||||
style={{
|
style={{
|
||||||
flexDirection: 'column'
|
width: 'match_parent',
|
||||||
}}>
|
borderLeftWidth: 20,
|
||||||
<TextWidget
|
borderLeftColor: GetTaskColor(task) as ColorProp,
|
||||||
text={task.title}
|
alignItems: 'center',
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
paddingVertical: 16,
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
borderRadius: 8,
|
||||||
|
backgroundColor: '#333333',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FlexWidget
|
||||||
style={{
|
style={{
|
||||||
color: '#ffffff',
|
flexDirection: 'column'
|
||||||
fontSize: 20,
|
}}>
|
||||||
fontWeight: 'bold',
|
<TextWidget
|
||||||
}}
|
text={task.title}
|
||||||
/>
|
style={{
|
||||||
<TextWidget
|
color: '#ffffff',
|
||||||
text={categories?.find(c => c.id === task.category)?.title || 'Uncategorized'}
|
fontSize: 20,
|
||||||
style={{
|
fontWeight: 'bold',
|
||||||
color: '#ffffff',
|
}}
|
||||||
fontSize: 16,
|
/>
|
||||||
}}
|
<TextWidget
|
||||||
/>
|
text={`${category ? (category.icon + " " + category.title) : 'Uncategorized'}`}
|
||||||
|
style={{
|
||||||
|
color: '#ffffff',
|
||||||
|
fontSize: 12,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<TextWidget
|
||||||
|
text={parseInt(timeLeft.toString()) > 0 ? timeLeft + " left" : "Overdue"}
|
||||||
|
style={{
|
||||||
|
color: '#ffffff',
|
||||||
|
fontSize: 16,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FlexWidget>
|
||||||
|
<SvgWidget svg={getClockProgressSVG(
|
||||||
|
circleDegree,
|
||||||
|
100,
|
||||||
|
100,
|
||||||
|
GetTaskColor(task) as ColorProp
|
||||||
|
)} clickAction='UPDATE_TASK' clickActionData={{
|
||||||
|
id: task.id
|
||||||
|
}} />
|
||||||
</FlexWidget>
|
</FlexWidget>
|
||||||
<SvgWidget svg={getClockProgressSVG(
|
|
||||||
((task.daysToRedo! - (((Date.now() - task.lastDone!) / (24 * 3600 * 1000)))) / task.daysToRedo!) * 360, // Example calculation for degree
|
|
||||||
100,
|
|
||||||
100,
|
|
||||||
GetTaskColor(task) as ColorProp
|
|
||||||
)} clickAction='UPDATE_TASK' clickActionData={{
|
|
||||||
id: task.id
|
|
||||||
}} />
|
|
||||||
</FlexWidget>
|
</FlexWidget>
|
||||||
</FlexWidget>
|
)
|
||||||
))}
|
})}
|
||||||
{(tasks && tasks.length === 0) &&
|
{(tasks && tasks.length === 0) &&
|
||||||
<TextWidget text='No tasks created.' style={{
|
<TextWidget text='No tasks created.' style={{
|
||||||
color: '#ffffff'
|
color: '#ffffff'
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { Task, TaskQuery } from '../../models/task';
|
|||||||
import { CategoryRepository } from '../../repositories/CategoryRepository';
|
import { CategoryRepository } from '../../repositories/CategoryRepository';
|
||||||
import { TaskRepository } from '../../repositories/TaskRepository';
|
import { TaskRepository } from '../../repositories/TaskRepository';
|
||||||
import { SQLiteDataService } from '../../services/data/sqliteDataService';
|
import { SQLiteDataService } from '../../services/data/sqliteDataService';
|
||||||
|
import { throttle } from '../../utils/throttle';
|
||||||
import { HelloWidget } from './hello';
|
import { HelloWidget } from './hello';
|
||||||
|
|
||||||
|
|
||||||
@ -50,9 +51,9 @@ notify_listener = Notifications.addNotificationReceivedListener(notification =>
|
|||||||
|
|
||||||
notify_responseListener = Notifications.addNotificationResponseReceivedListener(response => { });
|
notify_responseListener = Notifications.addNotificationResponseReceivedListener(response => { });
|
||||||
|
|
||||||
const CheckTaskAndNotify = async (): Promise<{ tasks: Task[], categories: Category[] }> => {
|
const CheckTaskAndNotify = async (): Promise<{ newTasks: Task[], newCategories: Category[] }> => {
|
||||||
tasks = await taskRepository.findAll();
|
const tasks = await taskRepository.findAll();
|
||||||
categories = await categoryRepository.findAll();
|
const categories = await categoryRepository.findAll();
|
||||||
|
|
||||||
const expiredTasks = tasks.filter(task => {
|
const expiredTasks = tasks.filter(task => {
|
||||||
const expirationDate = task.lastDone! + (task.daysToRedo! * 24 * 60 * 60 * 1000); // Convert days to milliseconds
|
const expirationDate = task.lastDone! + (task.daysToRedo! * 24 * 60 * 60 * 1000); // Convert days to milliseconds
|
||||||
@ -74,19 +75,30 @@ const CheckTaskAndNotify = async (): Promise<{ tasks: Task[], categories: Catego
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { tasks, categories };
|
return { newTasks: tasks, newCategories: categories };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const throttledCheckTaskAndNotify = throttle(CheckTaskAndNotify, 1000); // Throttle to prevent too frequent calls
|
||||||
|
|
||||||
setInterval(async () => {
|
setInterval(async () => {
|
||||||
CheckTaskAndNotify();
|
const { newTasks, newCategories } = await throttledCheckTaskAndNotify();
|
||||||
|
console.log("Throttled task check and notify executed OUT");
|
||||||
|
console.log("New tasks:", newTasks);
|
||||||
|
console.log("New categories:", newCategories);
|
||||||
|
tasks = newTasks;
|
||||||
|
categories = newCategories;
|
||||||
}, 1000 * 60 * 30); // Check every 30 minutes
|
}, 1000 * 60 * 30); // Check every 30 minutes
|
||||||
|
|
||||||
|
|
||||||
export async function widgetTaskHandler(props: WidgetTaskHandlerProps) {
|
export async function widgetTaskHandler(props: WidgetTaskHandlerProps) {
|
||||||
|
|
||||||
async function updateAndRenderWidget() {
|
async function updateAndRenderWidget() {
|
||||||
const { tasks, categories } = await CheckTaskAndNotify();
|
const { newTasks, newCategories } = await throttledCheckTaskAndNotify();
|
||||||
|
tasks = newTasks;
|
||||||
|
categories = newCategories;
|
||||||
|
console.log("Throttled task check and notify executed IN");
|
||||||
|
console.log("New tasks:", newTasks);
|
||||||
|
console.log("New categories:", newCategories);
|
||||||
props.renderWidget(<HelloWidget tasks={tasks} categories={categories} />);
|
props.renderWidget(<HelloWidget tasks={tasks} categories={categories} />);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -23,6 +23,16 @@ export type TaskItemProps = ViewProps & {
|
|||||||
|
|
||||||
const taskRepository = new TaskRepository(new SQLiteDataService<Task>(TaskQuery));
|
const taskRepository = new TaskRepository(new SQLiteDataService<Task>(TaskQuery));
|
||||||
|
|
||||||
|
const adaptDaysToGoToUnit = (t: Task) => {
|
||||||
|
const timeLeft = t.lastDone! + (t.daysToRedo! * 24 * 60 * 60 * 1000) - Date.now();
|
||||||
|
if (timeLeft < 0) return 0; // Task is overdue
|
||||||
|
else if (timeLeft < 3 * 60 * 60 * 1000) return Math.ceil(timeLeft / (60 * 1000)) + " minutes";
|
||||||
|
else if (timeLeft < 24 * 60 * 60 * 1000) return Math.ceil(timeLeft / (3600 * 1000)) + " hours";
|
||||||
|
else if (timeLeft < 7 * 24 * 60 * 60 * 1000) return Math.ceil(timeLeft / (24 * 3600 * 1000)) + " days";
|
||||||
|
else if (timeLeft < 30 * 24 * 60 * 60 * 1000) return Math.ceil(timeLeft / (7 * 24 * 3600 * 1000)) + " weeks";
|
||||||
|
else return Math.ceil(timeLeft / (30 * 24 * 3600 * 1000)) + " months";
|
||||||
|
};
|
||||||
|
|
||||||
export function TaskItem({ task, categories, onUpdate, lightColor, darkColor, ...otherProps }: TaskItemProps) {
|
export function TaskItem({ task, categories, onUpdate, lightColor, darkColor, ...otherProps }: TaskItemProps) {
|
||||||
const backgroundColor = useThemeColor({ light: lightColor, dark: darkColor }, 'background');
|
const backgroundColor = useThemeColor({ light: lightColor, dark: darkColor }, 'background');
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -79,7 +89,7 @@ export function TaskItem({ task, categories, onUpdate, lightColor, darkColor, ..
|
|||||||
{task?.title}
|
{task?.title}
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={styles(colorScheme).taskItemText}>
|
<Text style={styles(colorScheme).taskItemText}>
|
||||||
{category?.icon + " " + category?.title + " - " + Math.ceil(daysLeft.current / (3600 * 1000))} hours left
|
{category?.icon + " " + category?.title + " - " + (adaptDaysToGoToUnit(task) == 0 ? "Overdue" : adaptDaysToGoToUnit(task))}
|
||||||
</Text>
|
</Text>
|
||||||
</ThemedView>
|
</ThemedView>
|
||||||
<Pressable style={styles(colorScheme).checkButton} onPress={() => {
|
<Pressable style={styles(colorScheme).checkButton} onPress={() => {
|
||||||
@ -92,7 +102,7 @@ export function TaskItem({ task, categories, onUpdate, lightColor, darkColor, ..
|
|||||||
console.error("Error updating task:", error);
|
console.error("Error updating task:", error);
|
||||||
});
|
});
|
||||||
}}>
|
}}>
|
||||||
<ClockProgress degree={((daysLeft.current / (24 * 3600 * 1000)) / task.daysToRedo!) * 360} width={40} height={40} color={GetTaskColor(task!)} />
|
<ClockProgress degree={parseInt(adaptDaysToGoToUnit(task).toString()) > 0 ? ((daysLeft.current / (24 * 3600 * 1000)) / task.daysToRedo!) * 360 : 0} width={40} height={40} color={GetTaskColor(task!)} />
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</ThemedView></Pressable>;
|
</ThemedView></Pressable>;
|
||||||
}
|
}
|
||||||
|
|||||||
23
i18n.js
Normal file
23
i18n.js
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import i18n from 'i18next';
|
||||||
|
import { initReactI18next } from 'react-i18next';
|
||||||
|
import * as RNLocalize from 'react-native-localize';
|
||||||
|
import en from './translations/en.json';
|
||||||
|
|
||||||
|
const resources = {
|
||||||
|
en: { translation: en },
|
||||||
|
};
|
||||||
|
|
||||||
|
const fallback = { languageTag: 'en', isRTL: false };
|
||||||
|
|
||||||
|
const { languageTag } = RNLocalize.findBestLanguageTag(Object.keys(resources)) || fallback;
|
||||||
|
|
||||||
|
i18n
|
||||||
|
.use(initReactI18next)
|
||||||
|
.init({
|
||||||
|
resources,
|
||||||
|
lng: languageTag,
|
||||||
|
fallbackLng: 'en',
|
||||||
|
interpolation: { escapeValue: false },
|
||||||
|
});
|
||||||
|
|
||||||
|
export default i18n;
|
||||||
@ -4,4 +4,5 @@ import { widgetTaskHandler } from './app/widgets/widget-task-handler';
|
|||||||
registerWidgetTaskHandler(widgetTaskHandler);
|
registerWidgetTaskHandler(widgetTaskHandler);
|
||||||
|
|
||||||
import 'expo-router/entry';
|
import 'expo-router/entry';
|
||||||
|
import './i18n.js';
|
||||||
|
|
||||||
|
|||||||
100
package-lock.json
generated
100
package-lock.json
generated
@ -32,13 +32,16 @@
|
|||||||
"expo-symbols": "~0.4.5",
|
"expo-symbols": "~0.4.5",
|
||||||
"expo-system-ui": "~5.0.10",
|
"expo-system-ui": "~5.0.10",
|
||||||
"expo-web-browser": "~14.2.0",
|
"expo-web-browser": "~14.2.0",
|
||||||
|
"i18next": "^25.3.2",
|
||||||
"react": "19.0.0",
|
"react": "19.0.0",
|
||||||
"react-dom": "19.0.0",
|
"react-dom": "19.0.0",
|
||||||
|
"react-i18next": "^15.6.1",
|
||||||
"react-native": "0.79.5",
|
"react-native": "0.79.5",
|
||||||
"react-native-android-widget": "^0.17.0",
|
"react-native-android-widget": "^0.17.0",
|
||||||
"react-native-emoji-popup": "^0.3.2",
|
"react-native-emoji-popup": "^0.3.2",
|
||||||
"react-native-gesture-handler": "~2.24.0",
|
"react-native-gesture-handler": "~2.24.0",
|
||||||
"react-native-input-select": "^2.1.7",
|
"react-native-input-select": "^2.1.7",
|
||||||
|
"react-native-localize": "^3.5.1",
|
||||||
"react-native-reanimated": "~3.17.4",
|
"react-native-reanimated": "~3.17.4",
|
||||||
"react-native-safe-area-context": "5.4.0",
|
"react-native-safe-area-context": "5.4.0",
|
||||||
"react-native-screens": "~4.11.1",
|
"react-native-screens": "~4.11.1",
|
||||||
@ -8491,6 +8494,15 @@
|
|||||||
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
|
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/html-parse-stringify": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"void-elements": "3.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/http-errors": {
|
"node_modules/http-errors": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
|
||||||
@ -8545,6 +8557,37 @@
|
|||||||
"integrity": "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==",
|
"integrity": "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==",
|
||||||
"license": "BSD-3-Clause"
|
"license": "BSD-3-Clause"
|
||||||
},
|
},
|
||||||
|
"node_modules/i18next": {
|
||||||
|
"version": "25.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/i18next/-/i18next-25.3.2.tgz",
|
||||||
|
"integrity": "sha512-JSnbZDxRVbphc5jiptxr3o2zocy5dEqpVm9qCGdJwRNO+9saUJS0/u4LnM/13C23fUEWxAylPqKU/NpMV/IjqA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://locize.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://locize.com/i18next.html"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.27.6"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": "^5"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"typescript": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/iconv-lite": {
|
"node_modules/iconv-lite": {
|
||||||
"version": "0.4.24",
|
"version": "0.4.24",
|
||||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
||||||
@ -11870,6 +11913,32 @@
|
|||||||
"react": ">=17.0.0"
|
"react": ">=17.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-i18next": {
|
||||||
|
"version": "15.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.6.1.tgz",
|
||||||
|
"integrity": "sha512-uGrzSsOUUe2sDBG/+FJq2J1MM+Y4368/QW8OLEKSFvnDflHBbZhSd1u3UkW0Z06rMhZmnB/AQrhCpYfE5/5XNg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.27.6",
|
||||||
|
"html-parse-stringify": "^3.0.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"i18next": ">= 23.2.3",
|
||||||
|
"react": ">= 16.8.0",
|
||||||
|
"typescript": "^5"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"react-dom": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react-native": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"typescript": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-is": {
|
"node_modules/react-is": {
|
||||||
"version": "19.1.0",
|
"version": "19.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-19.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-19.1.0.tgz",
|
||||||
@ -12009,6 +12078,26 @@
|
|||||||
"react-native": "*"
|
"react-native": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-native-localize": {
|
||||||
|
"version": "3.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-native-localize/-/react-native-localize-3.5.1.tgz",
|
||||||
|
"integrity": "sha512-AX2soRR1/BCK2GvzjIpH+0EQaR2acfojFtjAstnYBFPR7ThIQx5Xb+OlqpzYxcYTPDxIcU5i4/yMa23Agy7p8g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@expo/config-plugins": "^9.0.0 || ^10.0.0",
|
||||||
|
"react": "*",
|
||||||
|
"react-native": "*",
|
||||||
|
"react-native-macos": "*"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@expo/config-plugins": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react-native-macos": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-native-reanimated": {
|
"node_modules/react-native-reanimated": {
|
||||||
"version": "3.17.5",
|
"version": "3.17.5",
|
||||||
"resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-3.17.5.tgz",
|
"resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-3.17.5.tgz",
|
||||||
@ -14150,7 +14239,7 @@
|
|||||||
"version": "5.8.3",
|
"version": "5.8.3",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
||||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
@ -14446,6 +14535,15 @@
|
|||||||
"integrity": "sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w==",
|
"integrity": "sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/void-elements": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/walker": {
|
"node_modules/walker": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz",
|
||||||
|
|||||||
@ -27,6 +27,7 @@
|
|||||||
"expo-image": "~2.3.2",
|
"expo-image": "~2.3.2",
|
||||||
"expo-linking": "~7.1.7",
|
"expo-linking": "~7.1.7",
|
||||||
"expo-navigation-bar": "~4.2.7",
|
"expo-navigation-bar": "~4.2.7",
|
||||||
|
"expo-notifications": "~0.31.4",
|
||||||
"expo-router": "~5.1.3",
|
"expo-router": "~5.1.3",
|
||||||
"expo-splash-screen": "~0.30.10",
|
"expo-splash-screen": "~0.30.10",
|
||||||
"expo-sqlite": "~15.2.14",
|
"expo-sqlite": "~15.2.14",
|
||||||
@ -34,13 +35,16 @@
|
|||||||
"expo-symbols": "~0.4.5",
|
"expo-symbols": "~0.4.5",
|
||||||
"expo-system-ui": "~5.0.10",
|
"expo-system-ui": "~5.0.10",
|
||||||
"expo-web-browser": "~14.2.0",
|
"expo-web-browser": "~14.2.0",
|
||||||
|
"i18next": "^25.3.2",
|
||||||
"react": "19.0.0",
|
"react": "19.0.0",
|
||||||
"react-dom": "19.0.0",
|
"react-dom": "19.0.0",
|
||||||
|
"react-i18next": "^15.6.1",
|
||||||
"react-native": "0.79.5",
|
"react-native": "0.79.5",
|
||||||
"react-native-android-widget": "^0.17.0",
|
"react-native-android-widget": "^0.17.0",
|
||||||
"react-native-emoji-popup": "^0.3.2",
|
"react-native-emoji-popup": "^0.3.2",
|
||||||
"react-native-gesture-handler": "~2.24.0",
|
"react-native-gesture-handler": "~2.24.0",
|
||||||
"react-native-input-select": "^2.1.7",
|
"react-native-input-select": "^2.1.7",
|
||||||
|
"react-native-localize": "^3.5.1",
|
||||||
"react-native-reanimated": "~3.17.4",
|
"react-native-reanimated": "~3.17.4",
|
||||||
"react-native-safe-area-context": "5.4.0",
|
"react-native-safe-area-context": "5.4.0",
|
||||||
"react-native-screens": "~4.11.1",
|
"react-native-screens": "~4.11.1",
|
||||||
@ -48,8 +52,7 @@
|
|||||||
"react-native-vector-icons": "^10.2.0",
|
"react-native-vector-icons": "^10.2.0",
|
||||||
"react-native-web": "~0.20.0",
|
"react-native-web": "~0.20.0",
|
||||||
"react-native-webview": "13.13.5",
|
"react-native-webview": "13.13.5",
|
||||||
"sqlite": "^5.1.1",
|
"sqlite": "^5.1.1"
|
||||||
"expo-notifications": "~0.31.4"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.25.2",
|
"@babel/core": "^7.25.2",
|
||||||
|
|||||||
4
translations/en.json
Normal file
4
translations/en.json
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"welcome": "Welcome",
|
||||||
|
"hello_world": "Hello, World!"
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import { Task } from "@/models/task";
|
import { Task } from "../models/task";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
#85c75c
|
#85c75c
|
||||||
|
|||||||
12
utils/throttle.ts
Normal file
12
utils/throttle.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
export function throttle<T extends (...args: any[]) => any>(fn: T, delay: number): (...args: Parameters<T>) => ReturnType<T> {
|
||||||
|
let lastCall = 0;
|
||||||
|
let lastResult: ReturnType<T>;
|
||||||
|
return function (...args: Parameters<T>): ReturnType<T> {
|
||||||
|
const now = Date.now();
|
||||||
|
if (now - lastCall >= delay) {
|
||||||
|
lastCall = now;
|
||||||
|
lastResult = fn.apply(this, args);
|
||||||
|
}
|
||||||
|
return lastResult;
|
||||||
|
};
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user