Add basic CRUD for sets
This commit is contained in:
parent
73ce91f111
commit
bb9a6c5f37
86
Alarm.tsx
Normal file
86
Alarm.tsx
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
|
import React, {useEffect, useState} from 'react';
|
||||||
|
import {Button, Modal, StyleSheet, Text, View} from 'react-native';
|
||||||
|
import BackgroundTimer from 'react-native-background-timer';
|
||||||
|
|
||||||
|
export default function Alarm({onClose}: {onClose: () => void}) {
|
||||||
|
const [seconds, setSeconds] = useState(0);
|
||||||
|
const [minutes, setMinutes] = useState(0);
|
||||||
|
let intervalId: number;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
AsyncStorage.getItem('nextAlarm').then(async next => {
|
||||||
|
if (!next) return;
|
||||||
|
const ms = new Date(next).getTime() - new Date().getTime();
|
||||||
|
if (ms <= 0) return;
|
||||||
|
let secondsLeft = ms / 1000;
|
||||||
|
console.log({secondsLeft});
|
||||||
|
setSeconds(secondsLeft % 60);
|
||||||
|
setMinutes(Math.floor(secondsLeft / 60));
|
||||||
|
|
||||||
|
intervalId = setInterval(() => {
|
||||||
|
console.log({seconds, secondsLeft});
|
||||||
|
secondsLeft--;
|
||||||
|
if (secondsLeft <= 0) return clearInterval(intervalId);
|
||||||
|
setSeconds(Math.ceil(secondsLeft % 60));
|
||||||
|
setMinutes(Math.floor(secondsLeft / 60));
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
|
return () => clearInterval(intervalId);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const stop = () => {
|
||||||
|
BackgroundTimer.clearInterval(intervalId);
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
animationType="none"
|
||||||
|
transparent={true}
|
||||||
|
visible
|
||||||
|
onRequestClose={onClose}>
|
||||||
|
<View style={styles.modal}>
|
||||||
|
<Text style={styles.title}>Rest</Text>
|
||||||
|
<Text style={styles.timer}>
|
||||||
|
{minutes}:{seconds}
|
||||||
|
</Text>
|
||||||
|
<View style={{flexDirection: 'row'}}>
|
||||||
|
<View style={styles.button}>
|
||||||
|
<Button title="Close" color="#014B44" onPress={onClose} />
|
||||||
|
</View>
|
||||||
|
<View style={styles.button}>
|
||||||
|
<Button title="Stop" onPress={stop} />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
timer: {
|
||||||
|
marginBottom: 20,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
fontSize: 20,
|
||||||
|
marginBottom: 10,
|
||||||
|
},
|
||||||
|
modal: {
|
||||||
|
margin: 20,
|
||||||
|
backgroundColor: '#20232a',
|
||||||
|
padding: 20,
|
||||||
|
shadowColor: '#000',
|
||||||
|
shadowOffset: {
|
||||||
|
width: 0,
|
||||||
|
height: 2,
|
||||||
|
},
|
||||||
|
shadowOpacity: 0.25,
|
||||||
|
shadowRadius: 4,
|
||||||
|
elevation: 5,
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
button: {
|
||||||
|
marginRight: 10,
|
||||||
|
},
|
||||||
|
});
|
102
App.tsx
102
App.tsx
|
@ -1,65 +1,61 @@
|
||||||
import React, {useState} from 'react';
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
|
import {createBottomTabNavigator} from '@react-navigation/bottom-tabs';
|
||||||
import {
|
import {
|
||||||
Button,
|
DarkTheme,
|
||||||
SafeAreaView,
|
DefaultTheme,
|
||||||
StatusBar,
|
NavigationContainer,
|
||||||
TextInput,
|
} from '@react-navigation/native';
|
||||||
useColorScheme,
|
import React, {useEffect} from 'react';
|
||||||
Vibration,
|
import {StatusBar, useColorScheme} from 'react-native';
|
||||||
View,
|
import {setupSchema} from './db';
|
||||||
} from 'react-native';
|
import Exercises from './Exercises';
|
||||||
import BackgroundTimer from 'react-native-background-timer';
|
import Home from './Home';
|
||||||
import {Notifications} from 'react-native-notifications';
|
import Settings from './Settings';
|
||||||
import Sound from 'react-native-sound';
|
import Ionicons from 'react-native-vector-icons/Ionicons';
|
||||||
|
|
||||||
|
const Tab = createBottomTabNavigator<RootStackParamList>();
|
||||||
|
export type RootStackParamList = {
|
||||||
|
Home: {};
|
||||||
|
Exercises: {};
|
||||||
|
Settings: {};
|
||||||
|
Alarm: {};
|
||||||
|
};
|
||||||
|
|
||||||
|
setupSchema();
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
const dark = useColorScheme() === 'dark';
|
const dark = useColorScheme() === 'dark';
|
||||||
const alarm = new Sound('argon.mp3', Sound.MAIN_BUNDLE, error => {
|
|
||||||
if (error) throw new Error(error);
|
|
||||||
});
|
|
||||||
const [timer, setTimer] = useState('0');
|
|
||||||
|
|
||||||
Notifications.registerRemoteNotifications();
|
useEffect(() => {
|
||||||
Notifications.events().registerNotificationOpened(
|
AsyncStorage.getItem('minutes').then(async minutes => {
|
||||||
(notification, completion) => {
|
if (!minutes) await AsyncStorage.setItem('minutes', '3');
|
||||||
console.log('Notification opened:', notification);
|
|
||||||
alarm.stop();
|
|
||||||
Vibration.cancel();
|
|
||||||
completion();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const press = () => {
|
|
||||||
BackgroundTimer.setTimeout(() => {
|
|
||||||
alarm.play(_onEnd => Vibration.cancel());
|
|
||||||
Vibration.vibrate([0, 400, 600], /*repeat=*/ true);
|
|
||||||
Notifications.postLocalNotification({
|
|
||||||
title: 'title',
|
|
||||||
body: 'body',
|
|
||||||
badge: 1,
|
|
||||||
identifier: 'identifier',
|
|
||||||
payload: {},
|
|
||||||
sound: 'sound',
|
|
||||||
thread: 'thread',
|
|
||||||
type: 'type',
|
|
||||||
});
|
});
|
||||||
}, Number(timer));
|
}, []);
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={{flex: 1}}>
|
<NavigationContainer theme={dark ? DarkTheme : DefaultTheme}>
|
||||||
<StatusBar barStyle={dark ? 'light-content' : 'dark-content'} />
|
<StatusBar barStyle={dark ? 'light-content' : 'dark-content'} />
|
||||||
<View
|
<Tab.Navigator
|
||||||
style={{
|
screenOptions={({route}) => ({
|
||||||
margin: 10,
|
tabBarIcon: ({focused, color, size}) => {
|
||||||
alignItems: 'center',
|
let icon = '';
|
||||||
}}>
|
|
||||||
<TextInput placeholder="Timer" value={timer} onChangeText={setTimer} />
|
if (route.name === 'Home') icon = focused ? 'home' : 'home-outline';
|
||||||
</View>
|
else if (route.name === 'Settings')
|
||||||
<View style={{margin: 30, marginTop: 'auto'}}>
|
icon = focused ? 'settings' : 'settings-outline';
|
||||||
<Button title="Run timer" onPress={press} />
|
else if (route.name === 'Exercises')
|
||||||
</View>
|
icon = focused ? 'barbell' : 'barbell-outline';
|
||||||
</SafeAreaView>
|
// You can return any component that you like here!
|
||||||
|
return <Ionicons name={icon} size={size} color={color} />;
|
||||||
|
},
|
||||||
|
tabBarActiveTintColor: 'tomato',
|
||||||
|
tabBarInactiveTintColor: 'gray',
|
||||||
|
})}>
|
||||||
|
<Tab.Screen name="Home" component={Home} />
|
||||||
|
<Tab.Screen name="Exercises" component={Exercises} />
|
||||||
|
<Tab.Screen name="Settings" component={Settings} />
|
||||||
|
</Tab.Navigator>
|
||||||
|
</NavigationContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
152
EditSet.tsx
Normal file
152
EditSet.tsx
Normal file
|
@ -0,0 +1,152 @@
|
||||||
|
import React, {useEffect, useRef, useState} from 'react';
|
||||||
|
import {Modal, StyleSheet, Text, TextInput, View} from 'react-native';
|
||||||
|
import {Button} from 'react-native-paper';
|
||||||
|
import {getDb} from './db';
|
||||||
|
import Set from './Set';
|
||||||
|
|
||||||
|
export default function EditSet({
|
||||||
|
id,
|
||||||
|
onSave,
|
||||||
|
show,
|
||||||
|
setShow,
|
||||||
|
setId,
|
||||||
|
}: {
|
||||||
|
id?: number;
|
||||||
|
setId: (id?: number) => void;
|
||||||
|
onSave: () => void;
|
||||||
|
show: boolean;
|
||||||
|
setShow: (visible: boolean) => void;
|
||||||
|
}) {
|
||||||
|
const [name, setName] = useState('');
|
||||||
|
const [reps, setReps] = useState('');
|
||||||
|
const [weight, setWeight] = useState('');
|
||||||
|
const [unit, setUnit] = useState('');
|
||||||
|
const weightRef = useRef<TextInput>(null);
|
||||||
|
const repsRef = useRef<TextInput>(null);
|
||||||
|
const unitRef = useRef<TextInput>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!id) return;
|
||||||
|
getDb().then(async db => {
|
||||||
|
const [result] = await db.executeSql(`SELECT * FROM sets WHERE id = ?`, [
|
||||||
|
id,
|
||||||
|
]);
|
||||||
|
if (!result.rows.item(0)) throw new Error("Can't find specified Set.");
|
||||||
|
const set: Set = result.rows.item(0);
|
||||||
|
setName(set.name);
|
||||||
|
setReps(set.reps.toString());
|
||||||
|
setWeight(set.weight.toString());
|
||||||
|
setUnit(set.unit);
|
||||||
|
});
|
||||||
|
}, [id]);
|
||||||
|
|
||||||
|
const save = async () => {
|
||||||
|
if (!name || !reps || !weight) return;
|
||||||
|
const db = await getDb();
|
||||||
|
if (!id)
|
||||||
|
await db.executeSql(
|
||||||
|
`INSERT INTO sets(name, reps, weight, created, unit) VALUES (?,?,?,?,?)`,
|
||||||
|
[name, reps, weight, new Date().toISOString(), unit || 'kg'],
|
||||||
|
);
|
||||||
|
else
|
||||||
|
await db.executeSql(
|
||||||
|
`UPDATE sets SET name = ?, reps = ?, weight = ?, unit = ? WHERE id = ?`,
|
||||||
|
[name, reps, weight, unit, id],
|
||||||
|
);
|
||||||
|
setShow(false);
|
||||||
|
onSave();
|
||||||
|
};
|
||||||
|
|
||||||
|
const remove = async () => {
|
||||||
|
if (!id) return;
|
||||||
|
const db = await getDb();
|
||||||
|
await db.executeSql(`DELETE FROM sets WHERE id = ?`, [id]);
|
||||||
|
setShow(false);
|
||||||
|
onSave();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
<Modal
|
||||||
|
animationType="none"
|
||||||
|
transparent={true}
|
||||||
|
visible={show}
|
||||||
|
onRequestClose={() => setShow(false)}>
|
||||||
|
<View style={styles.modal}>
|
||||||
|
<Text style={styles.title}>Add a set</Text>
|
||||||
|
<TextInput
|
||||||
|
autoFocus
|
||||||
|
placeholder="Name *"
|
||||||
|
value={name}
|
||||||
|
onChangeText={setName}
|
||||||
|
onSubmitEditing={() => weightRef.current?.focus()}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
placeholder="Weight *"
|
||||||
|
keyboardType="numeric"
|
||||||
|
value={weight}
|
||||||
|
onChangeText={setWeight}
|
||||||
|
onSubmitEditing={() => repsRef.current?.focus()}
|
||||||
|
ref={weightRef}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
placeholder="Reps *"
|
||||||
|
keyboardType="numeric"
|
||||||
|
value={reps}
|
||||||
|
onChangeText={setReps}
|
||||||
|
ref={repsRef}
|
||||||
|
onSubmitEditing={() => unitRef.current?.focus()}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
placeholder="Unit (kg)"
|
||||||
|
value={unit}
|
||||||
|
onChangeText={setUnit}
|
||||||
|
ref={unitRef}
|
||||||
|
onSubmitEditing={save}
|
||||||
|
/>
|
||||||
|
<View style={styles.bottom}>
|
||||||
|
<Button mode="contained" icon="save" onPress={save}>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
<Button icon="close" onPress={() => setShow(false)}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button icon="trash" onPress={remove} disabled={!id}>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Modal>
|
||||||
|
<Button
|
||||||
|
mode="contained"
|
||||||
|
onPress={() => {
|
||||||
|
setId(undefined);
|
||||||
|
setShow(true);
|
||||||
|
}}>
|
||||||
|
Add
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
bottom: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
fontSize: 20,
|
||||||
|
},
|
||||||
|
modal: {
|
||||||
|
margin: 20,
|
||||||
|
backgroundColor: '#20232a',
|
||||||
|
padding: 20,
|
||||||
|
shadowColor: '#000',
|
||||||
|
shadowOffset: {
|
||||||
|
width: 0,
|
||||||
|
height: 2,
|
||||||
|
},
|
||||||
|
shadowOpacity: 0.25,
|
||||||
|
shadowRadius: 4,
|
||||||
|
elevation: 5,
|
||||||
|
},
|
||||||
|
});
|
15
Exercises.tsx
Normal file
15
Exercises.tsx
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import {NativeStackScreenProps} from '@react-navigation/native-stack';
|
||||||
|
import {Button, Text, View} from 'react-native';
|
||||||
|
import {RootStackParamList} from './App';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export default function Exercises({
|
||||||
|
navigation,
|
||||||
|
}: NativeStackScreenProps<RootStackParamList, 'Exercises'>) {
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
<Text>Pull ups - 1 rep</Text>
|
||||||
|
<Button title="Go home" onPress={() => navigation.navigate('Home', {})} />
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
188
Home.tsx
Normal file
188
Home.tsx
Normal file
|
@ -0,0 +1,188 @@
|
||||||
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
|
import {NativeStackScreenProps} from '@react-navigation/native-stack';
|
||||||
|
import React, {useEffect, useState} from 'react';
|
||||||
|
import {
|
||||||
|
FlatList,
|
||||||
|
SafeAreaView,
|
||||||
|
StyleSheet,
|
||||||
|
TextInput,
|
||||||
|
Vibration,
|
||||||
|
View,
|
||||||
|
} from 'react-native';
|
||||||
|
import BackgroundTimer from 'react-native-background-timer';
|
||||||
|
import {Button, List} from 'react-native-paper';
|
||||||
|
import PushNotification from 'react-native-push-notification';
|
||||||
|
import Sound from 'react-native-sound';
|
||||||
|
import Alarm from './Alarm';
|
||||||
|
import {RootStackParamList} from './App';
|
||||||
|
import {ALARM} from './channels';
|
||||||
|
import {getDb} from './db';
|
||||||
|
import EditSet from './EditSet';
|
||||||
|
|
||||||
|
import Set from './Set';
|
||||||
|
|
||||||
|
const limit = 20;
|
||||||
|
|
||||||
|
export default function Home({
|
||||||
|
navigation,
|
||||||
|
}: NativeStackScreenProps<RootStackParamList, 'Home'>) {
|
||||||
|
const [sets, setSets] = useState<Set[]>();
|
||||||
|
const [id, setId] = useState<number>();
|
||||||
|
const [offset, setOffset] = useState(0);
|
||||||
|
const [refreshing, setRefreshing] = useState(false);
|
||||||
|
const [showTimer, setShowTimer] = useState(false);
|
||||||
|
const [showEdit, setShowEdit] = useState(false);
|
||||||
|
const [search, setSearch] = useState('');
|
||||||
|
|
||||||
|
const refresh = async () => {
|
||||||
|
setRefreshing(true);
|
||||||
|
const db = await getDb();
|
||||||
|
const [result] = await db.executeSql(
|
||||||
|
`SELECT * from sets WHERE name LIKE ? LIMIT ? OFFSET ?`,
|
||||||
|
[`%${search}%`, limit, 0],
|
||||||
|
);
|
||||||
|
setRefreshing(false);
|
||||||
|
if (!result) return setSets([]);
|
||||||
|
setSets(result.rows.raw());
|
||||||
|
setOffset(0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const alarm = new Sound('argon.mp3', Sound.MAIN_BUNDLE, error => {
|
||||||
|
if (error) throw new Error(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
const focus = async () => {
|
||||||
|
alarm.stop();
|
||||||
|
Vibration.cancel();
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
refresh();
|
||||||
|
}, [search]);
|
||||||
|
|
||||||
|
useEffect(() => navigation.addListener('focus', focus), [navigation]);
|
||||||
|
|
||||||
|
const renderItem = ({item}: {item: Set}) => (
|
||||||
|
<List.Item
|
||||||
|
onPress={() => {
|
||||||
|
setId(item.id);
|
||||||
|
setShowEdit(true);
|
||||||
|
}}
|
||||||
|
key={item.id}
|
||||||
|
title={item.name}
|
||||||
|
description={`${item.reps} x ${item.weight}${item.unit}`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const save = async () => {
|
||||||
|
refresh();
|
||||||
|
const enabled = await AsyncStorage.getItem('alarmEnabled');
|
||||||
|
console.log({enabled});
|
||||||
|
if (enabled !== 'true') return;
|
||||||
|
const minutes = await AsyncStorage.getItem('minutes');
|
||||||
|
const seconds = await AsyncStorage.getItem('seconds');
|
||||||
|
const milliseconds = Number(minutes) * 60 * 1000 + Number(seconds) * 1000;
|
||||||
|
const when = new Date();
|
||||||
|
when.setTime(when.getTime() + milliseconds);
|
||||||
|
await AsyncStorage.setItem('nextAlarm', when.toISOString());
|
||||||
|
const timeoutId = BackgroundTimer.setTimeout(() => {
|
||||||
|
alarm.play(_onEnd => Vibration.cancel());
|
||||||
|
Vibration.vibrate([0, 400, 600], /*repeat=*/ true);
|
||||||
|
PushNotification.localNotification({
|
||||||
|
message: 'Timer up',
|
||||||
|
channelId: ALARM,
|
||||||
|
vibrate: true,
|
||||||
|
});
|
||||||
|
}, Number(milliseconds));
|
||||||
|
BackgroundTimer.clearTimeout(
|
||||||
|
Number(await AsyncStorage.getItem('timeoutId')),
|
||||||
|
);
|
||||||
|
await AsyncStorage.setItem('timeoutId', timeoutId.toString());
|
||||||
|
};
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
alarm.stop();
|
||||||
|
Vibration.cancel();
|
||||||
|
setShowTimer(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const next = async () => {
|
||||||
|
const newOffset = offset + limit;
|
||||||
|
setRefreshing(true);
|
||||||
|
const db = await getDb();
|
||||||
|
const [result] = await db.executeSql(
|
||||||
|
`SELECT * from sets WHERE name LIKE ? LIMIT ? OFFSET ?`,
|
||||||
|
[`%${search}%`, limit, newOffset],
|
||||||
|
);
|
||||||
|
setRefreshing(false);
|
||||||
|
if (!result) return;
|
||||||
|
if (!sets) return;
|
||||||
|
setSets([...sets, ...result.rows.raw()]);
|
||||||
|
setOffset(newOffset);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SafeAreaView style={styles.container}>
|
||||||
|
<TextInput placeholder="Search" value={search} onChangeText={setSearch} />
|
||||||
|
<FlatList
|
||||||
|
style={{height: '100%'}}
|
||||||
|
data={sets}
|
||||||
|
renderItem={renderItem}
|
||||||
|
keyExtractor={set => set.id.toString()}
|
||||||
|
onRefresh={refresh}
|
||||||
|
refreshing={refreshing}
|
||||||
|
onScrollEndDrag={next}
|
||||||
|
/>
|
||||||
|
<View style={styles.bottom}>
|
||||||
|
<View style={styles.button}></View>
|
||||||
|
<View style={styles.button}>
|
||||||
|
<Button icon="time" onPress={() => setShowTimer(true)}>
|
||||||
|
Time left
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
<View style={styles.button}>
|
||||||
|
<Button icon="stop" onPress={close}>
|
||||||
|
Stop
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
<View style={styles.button}>
|
||||||
|
<EditSet
|
||||||
|
id={id}
|
||||||
|
setId={setId}
|
||||||
|
show={showEdit}
|
||||||
|
setShow={setShowEdit}
|
||||||
|
onSave={save}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
{showTimer && <Alarm onClose={close} />}
|
||||||
|
</SafeAreaView>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
name: {
|
||||||
|
fontSize: 18,
|
||||||
|
},
|
||||||
|
button: {
|
||||||
|
marginRight: 10,
|
||||||
|
},
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
paddingLeft: 20,
|
||||||
|
paddingRight: 20,
|
||||||
|
},
|
||||||
|
bottom: {
|
||||||
|
alignSelf: 'center',
|
||||||
|
marginBottom: 10,
|
||||||
|
flexDirection: 'row',
|
||||||
|
},
|
||||||
|
set: {
|
||||||
|
marginBottom: 10,
|
||||||
|
fontSize: 18,
|
||||||
|
shadowColor: 'red',
|
||||||
|
shadowRadius: 10,
|
||||||
|
shadowOffset: {width: 2, height: 40},
|
||||||
|
shadowOpacity: 8,
|
||||||
|
},
|
||||||
|
});
|
8
Set.ts
Normal file
8
Set.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
export default interface Set {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
reps: number;
|
||||||
|
weight: number;
|
||||||
|
created: string;
|
||||||
|
unit: string;
|
||||||
|
}
|
69
Settings.tsx
Normal file
69
Settings.tsx
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
|
import {NativeStackScreenProps} from '@react-navigation/native-stack';
|
||||||
|
import React, {useEffect, useState} from 'react';
|
||||||
|
import {StyleSheet, Switch, Text, TextInput, View} from 'react-native';
|
||||||
|
import {Button} from 'react-native-paper';
|
||||||
|
import {RootStackParamList} from './App';
|
||||||
|
import {getDb} from './db';
|
||||||
|
|
||||||
|
export default function Settings({
|
||||||
|
navigation,
|
||||||
|
}: NativeStackScreenProps<RootStackParamList, 'Settings'>) {
|
||||||
|
const [minutes, setMinutes] = useState<string>('');
|
||||||
|
const [seconds, setSeconds] = useState<string>('');
|
||||||
|
const [alarmEnabled, setAlarmEnabled] = useState<boolean>(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
setMinutes((await AsyncStorage.getItem('minutes')) || '3');
|
||||||
|
setSeconds((await AsyncStorage.getItem('seconds')) || '');
|
||||||
|
setAlarmEnabled((await AsyncStorage.getItem('alarmEnabled')) === 'true');
|
||||||
|
})();
|
||||||
|
}, [navigation]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (minutes) AsyncStorage.setItem('minutes', minutes);
|
||||||
|
if (seconds) AsyncStorage.setItem('seconds', seconds);
|
||||||
|
AsyncStorage.setItem('alarmEnabled', alarmEnabled ? 'true' : 'false');
|
||||||
|
}, [minutes, seconds, alarmEnabled]);
|
||||||
|
|
||||||
|
const clear = async () => {
|
||||||
|
const db = await getDb();
|
||||||
|
await db.executeSql(`DELETE FROM sets`);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<Text>Rest minutes</Text>
|
||||||
|
<TextInput
|
||||||
|
value={minutes}
|
||||||
|
keyboardType="numeric"
|
||||||
|
placeholder="3"
|
||||||
|
onChangeText={setMinutes}
|
||||||
|
/>
|
||||||
|
<Text>Rest seconds</Text>
|
||||||
|
<TextInput
|
||||||
|
value={seconds}
|
||||||
|
keyboardType="numeric"
|
||||||
|
placeholder="30"
|
||||||
|
onChangeText={setSeconds}
|
||||||
|
/>
|
||||||
|
<Text>Alarm enabled?</Text>
|
||||||
|
<Switch
|
||||||
|
style={{alignSelf: 'flex-start'}}
|
||||||
|
value={alarmEnabled}
|
||||||
|
onValueChange={setAlarmEnabled}
|
||||||
|
/>
|
||||||
|
<Button icon="trash" onPress={clear}>
|
||||||
|
Clear sets
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
padding: 10,
|
||||||
|
},
|
||||||
|
});
|
|
@ -82,6 +82,7 @@ project.ext.react = [
|
||||||
]
|
]
|
||||||
|
|
||||||
apply from: "../../node_modules/react-native/react.gradle"
|
apply from: "../../node_modules/react-native/react.gradle"
|
||||||
|
apply from: "../../node_modules/react-native-vector-icons/fonts.gradle"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set this to true to create two separate APKs instead of one:
|
* Set this to true to create two separate APKs instead of one:
|
||||||
|
@ -129,6 +130,12 @@ def reactNativeArchitectures() {
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
packagingOptions {
|
||||||
|
pickFirst '**/armeabi-v7a/libfolly_runtime.so'
|
||||||
|
pickFirst '**/x86/libfolly_runtime.so'
|
||||||
|
pickFirst '**/arm64-v8a/libfolly_runtime.so'
|
||||||
|
pickFirst '**/x86_64/libfolly_runtime.so'
|
||||||
|
}
|
||||||
ndkVersion rootProject.ext.ndkVersion
|
ndkVersion rootProject.ext.ndkVersion
|
||||||
|
|
||||||
compileSdkVersion rootProject.ext.compileSdkVersion
|
compileSdkVersion rootProject.ext.compileSdkVersion
|
||||||
|
@ -262,7 +269,7 @@ dependencies {
|
||||||
//noinspection GradleDynamicVersion
|
//noinspection GradleDynamicVersion
|
||||||
implementation "com.facebook.react:react-native:+" // From node_modules
|
implementation "com.facebook.react:react-native:+" // From node_modules
|
||||||
|
|
||||||
implementation project(':react-native-notifications');
|
implementation project(':react-native-sqlite-storage')
|
||||||
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0"
|
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0"
|
||||||
|
|
||||||
debugImplementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}") {
|
debugImplementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}") {
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
package="com.massive">
|
package="com.massive">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.VIBRATE" />
|
<uses-permission android:name="android.permission.VIBRATE" />
|
||||||
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".MainApplication"
|
android:name=".MainApplication"
|
||||||
|
@ -10,6 +11,7 @@
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:allowBackup="false"
|
android:allowBackup="false"
|
||||||
android:theme="@style/AppTheme">
|
android:theme="@style/AppTheme">
|
||||||
|
<meta-data android:name="com.dieam.reactnativepushnotification.notification_foreground" android:value="false"/>
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
|
|
|
@ -12,7 +12,7 @@ import com.facebook.soloader.SoLoader;
|
||||||
import com.massive.newarchitecture.MainApplicationReactNativeHost;
|
import com.massive.newarchitecture.MainApplicationReactNativeHost;
|
||||||
import java.lang.reflect.InvocationTargetException;
|
import java.lang.reflect.InvocationTargetException;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import com.wix.reactnativenotifications.RNNotificationsPackage;
|
import org.pgsqlite.SQLitePluginPackage;
|
||||||
|
|
||||||
public class MainApplication extends Application implements ReactApplication {
|
public class MainApplication extends Application implements ReactApplication {
|
||||||
|
|
||||||
|
@ -27,9 +27,7 @@ public class MainApplication extends Application implements ReactApplication {
|
||||||
protected List<ReactPackage> getPackages() {
|
protected List<ReactPackage> getPackages() {
|
||||||
@SuppressWarnings("UnnecessaryLocalVariable")
|
@SuppressWarnings("UnnecessaryLocalVariable")
|
||||||
List<ReactPackage> packages = new PackageList(this).getPackages();
|
List<ReactPackage> packages = new PackageList(this).getPackages();
|
||||||
// Packages that cannot be autolinked yet can be added manually here, for example:
|
packages.add(new SQLitePluginPackage());
|
||||||
// packages.add(new MyReactNativePackage());
|
|
||||||
packages.add(new RNNotificationsPackage(MainApplication.this));
|
|
||||||
return packages;
|
return packages;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,8 +2,8 @@ rootProject.name = 'massive'
|
||||||
apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings)
|
apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings)
|
||||||
include ':app'
|
include ':app'
|
||||||
includeBuild('../node_modules/react-native-gradle-plugin')
|
includeBuild('../node_modules/react-native-gradle-plugin')
|
||||||
include ':react-native-notifications'
|
include ':react-native-sqlite-storage'
|
||||||
project(':react-native-notifications').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-notifications/lib/android/app')
|
project(':react-native-sqlite-storage').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-sqlite-storage/platforms/android')
|
||||||
|
|
||||||
if (settings.hasProperty("newArchEnabled") && settings.newArchEnabled == "true") {
|
if (settings.hasProperty("newArchEnabled") && settings.newArchEnabled == "true") {
|
||||||
include(":ReactAndroid")
|
include(":ReactAndroid")
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
presets: ['module:metro-react-native-babel-preset'],
|
presets: ['module:metro-react-native-babel-preset'],
|
||||||
|
plugins: ['react-native-reanimated/plugin', 'react-native-paper/babel'],
|
||||||
};
|
};
|
||||||
|
|
13
channels.ts
Normal file
13
channels.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import PushNotification, {Importance} from 'react-native-push-notification';
|
||||||
|
|
||||||
|
export const ALARM = 'alarm';
|
||||||
|
PushNotification.createChannel(
|
||||||
|
{
|
||||||
|
channelId: ALARM,
|
||||||
|
channelName: 'Alarms',
|
||||||
|
channelDescription: 'Notifications of when alarms are triggered.',
|
||||||
|
importance: Importance.HIGH,
|
||||||
|
vibrate: false,
|
||||||
|
},
|
||||||
|
created => console.log(`Created channel ${created}`),
|
||||||
|
);
|
17
db.ts
Normal file
17
db.ts
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import {enablePromise, openDatabase} from 'react-native-sqlite-storage';
|
||||||
|
|
||||||
|
enablePromise(true);
|
||||||
|
export const getDb = () => openDatabase({name: 'massive.db'});
|
||||||
|
|
||||||
|
const schema = `
|
||||||
|
CREATE TABLE IF NOT EXISTS sets (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
reps INTEGER NOT NULL,
|
||||||
|
weight INTEGER NOT NULL,
|
||||||
|
created TEXT NOT NULL,
|
||||||
|
unit TEXT DEFAULT 'kg'
|
||||||
|
);
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const setupSchema = () => getDb().then(db => db.executeSql(schema));
|
44
index.js
44
index.js
|
@ -1,9 +1,43 @@
|
||||||
/**
|
import 'react-native-gesture-handler';
|
||||||
* @format
|
import 'react-native-sqlite-storage';
|
||||||
*/
|
import React from 'react';
|
||||||
|
|
||||||
import {AppRegistry} from 'react-native';
|
import {AppRegistry} from 'react-native';
|
||||||
import App from './App';
|
import App from './App';
|
||||||
import {name as appName} from './app.json';
|
import {name as appName} from './app.json';
|
||||||
|
import PushNotification from 'react-native-push-notification';
|
||||||
|
import {Provider as PaperProvider, DarkTheme} from 'react-native-paper';
|
||||||
|
import Ionicon from 'react-native-vector-icons/Ionicons';
|
||||||
|
|
||||||
AppRegistry.registerComponent(appName, () => App);
|
export default function Main() {
|
||||||
|
return (
|
||||||
|
<PaperProvider
|
||||||
|
theme={DarkTheme}
|
||||||
|
settings={{icon: props => <Ionicon {...props} />}}>
|
||||||
|
<App />
|
||||||
|
</PaperProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
AppRegistry.registerComponent(appName, () => Main);
|
||||||
|
|
||||||
|
PushNotification.configure({
|
||||||
|
onRegister: function (token) {
|
||||||
|
console.log('TOKEN:', token);
|
||||||
|
},
|
||||||
|
|
||||||
|
onNotification: function (notification) {
|
||||||
|
console.log('NOTIFICATION:', notification);
|
||||||
|
},
|
||||||
|
|
||||||
|
onAction: function (notification) {
|
||||||
|
console.log('ACTION:', notification.action);
|
||||||
|
console.log('NOTIFICATION:', notification);
|
||||||
|
},
|
||||||
|
|
||||||
|
onRegistrationError: function (err) {
|
||||||
|
console.error(err.message, err);
|
||||||
|
},
|
||||||
|
|
||||||
|
popInitialNotification: true,
|
||||||
|
requestPermissions: false,
|
||||||
|
});
|
||||||
|
|
21
package.json
21
package.json
|
@ -10,12 +10,29 @@
|
||||||
"lint": "eslint . --ext .js,.jsx,.ts,.tsx"
|
"lint": "eslint . --ext .js,.jsx,.ts,.tsx"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@babel/preset-env": "^7.1.6",
|
||||||
|
"@react-native-async-storage/async-storage": "^1.17.7",
|
||||||
|
"@react-native-community/push-notification-ios": "^1.10.1",
|
||||||
|
"@react-navigation/bottom-tabs": "^6.3.1",
|
||||||
|
"@react-navigation/native": "^6.0.10",
|
||||||
|
"@react-navigation/native-stack": "^6.6.2",
|
||||||
"@types/react-native-background-timer": "^2.0.0",
|
"@types/react-native-background-timer": "^2.0.0",
|
||||||
|
"@types/react-native-push-notification": "^8.1.1",
|
||||||
|
"@types/react-native-sqlite-storage": "^5.0.2",
|
||||||
|
"@types/react-native-vector-icons": "^6.4.11",
|
||||||
"react": "18.0.0",
|
"react": "18.0.0",
|
||||||
|
"react-devtools": "^4.24.7",
|
||||||
"react-native": "0.69.1",
|
"react-native": "0.69.1",
|
||||||
"react-native-background-timer": "^2.4.1",
|
"react-native-background-timer": "^2.4.1",
|
||||||
"react-native-notifications": "^4.3.1",
|
"react-native-gesture-handler": "^2.5.0",
|
||||||
"react-native-sound": "^0.11.2"
|
"react-native-paper": "^4.12.2",
|
||||||
|
"react-native-push-notification": "^8.1.1",
|
||||||
|
"react-native-reanimated": "^2.9.0",
|
||||||
|
"react-native-safe-area-context": "^4.3.1",
|
||||||
|
"react-native-screens": "^3.14.0",
|
||||||
|
"react-native-sound": "^0.11.2",
|
||||||
|
"react-native-sqlite-storage": "^6.0.1",
|
||||||
|
"react-native-vector-icons": "^9.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.12.9",
|
"@babel/core": "^7.12.9",
|
||||||
|
|
Loading…
Reference in New Issue
Block a user