Move to top bar navigation

This commit is contained in:
Brandon Presley 2022-07-04 16:03:48 +12:00
parent 00a801b44a
commit 5a1257a859
9 changed files with 158 additions and 191 deletions

View File

@ -1,86 +1,59 @@
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';
import {StyleSheet, Text} from 'react-native';
import {Button, Modal, Portal} from 'react-native-paper';
export default function Alarm({onClose}: {onClose: () => void}) {
export default function Alarm() {
const [show, setShow] = useState(false);
const [seconds, setSeconds] = useState(0);
const [minutes, setMinutes] = useState(0);
let intervalId: number;
useEffect(() => {
AsyncStorage.getItem('nextAlarm').then(async next => {
intervalId = setInterval(async () => {
const next = await AsyncStorage.getItem('nextAlarm');
if (!next) return;
const ms = new Date(next).getTime() - new Date().getTime();
if (ms <= 0) return;
let secondsLeft = ms / 1000;
console.log({secondsLeft});
secondsLeft--;
if (secondsLeft <= 0) return clearInterval(intervalId);
setSeconds(Math.floor(secondsLeft % 60));
setMinutes(Math.floor(secondsLeft / 60));
intervalId = setInterval(() => {
console.log({seconds, secondsLeft});
secondsLeft--;
if (secondsLeft <= 0) return clearInterval(intervalId);
setSeconds(Math.floor(secondsLeft % 60));
setMinutes(Math.floor(secondsLeft / 60));
}, 1000);
});
}, 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>
<>
<Portal>
<Modal
visible={show}
style={styles.center}
onDismiss={() => setShow(false)}>
<Text style={[styles.center, styles.title]}>Resting</Text>
<Text style={styles.center}>
{minutes}:{seconds}
</Text>
<Button mode="contained" onPress={() => setShow(false)}>
Close
</Button>
</Modal>
</Portal>
<Button icon="time" onPress={() => setShow(true)}>
Time left
</Button>
</>
);
}
const styles = StyleSheet.create({
timer: {
marginBottom: 20,
},
title: {
fontSize: 20,
center: {
alignItems: 'center',
alignSelf: 'center',
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,
title: {
fontSize: 18,
},
});

25
App.tsx
View File

@ -1,5 +1,5 @@
import AsyncStorage from '@react-native-async-storage/async-storage';
import {createBottomTabNavigator} from '@react-navigation/bottom-tabs';
import {createMaterialTopTabNavigator} from '@react-navigation/material-top-tabs';
import {
DarkTheme,
DefaultTheme,
@ -11,14 +11,12 @@ import {setupSchema} from './db';
import Exercises from './Exercises';
import Home from './Home';
import Settings from './Settings';
import Ionicons from 'react-native-vector-icons/Ionicons';
const Tab = createBottomTabNavigator<RootStackParamList>();
const Tab = createMaterialTopTabNavigator<RootStackParamList>();
export type RootStackParamList = {
Home: {};
Exercises: {};
Settings: {};
Alarm: {};
Exercises: {};
};
setupSchema();
@ -35,22 +33,7 @@ const App = () => {
return (
<NavigationContainer theme={dark ? DarkTheme : DefaultTheme}>
<StatusBar barStyle={dark ? 'light-content' : 'dark-content'} />
<Tab.Navigator
screenOptions={({route}) => ({
tabBarIcon: ({focused, color, size}) => {
let icon = '';
if (route.name === 'Home') icon = focused ? 'home' : 'home-outline';
else if (route.name === 'Settings')
icon = focused ? 'settings' : 'settings-outline';
else if (route.name === 'Exercises')
icon = focused ? 'barbell' : 'barbell-outline';
// You can return any component that you like here!
return <Ionicons name={icon} size={size} color={color} />;
},
tabBarActiveTintColor: 'tomato',
tabBarInactiveTintColor: 'gray',
})}>
<Tab.Navigator>
<Tab.Screen name="Home" component={Home} />
<Tab.Screen name="Exercises" component={Exercises} />
<Tab.Screen name="Settings" component={Settings} />

View File

@ -1,8 +1,8 @@
import React, {useEffect, useRef, useState} from 'react';
import {Modal, StyleSheet, Text, TextInput, View} from 'react-native';
import {Button} from 'react-native-paper';
import {StyleSheet, Text, View} from 'react-native';
import {Button, Modal, Portal, TextInput} from 'react-native-paper';
import {getDb} from './db';
import Set from './Set';
import Set from './set';
export default function EditSet({
id,
@ -21,9 +21,9 @@ export default function EditSet({
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);
const weightRef = useRef<any>(null);
const repsRef = useRef<any>(null);
const unitRef = useRef<any>(null);
useEffect(() => {
if (!id) return;
@ -66,39 +66,42 @@ export default function EditSet({
};
return (
<View>
<Modal
animationType="none"
transparent={true}
visible={show}
onRequestClose={() => setShow(false)}>
<View style={styles.modal}>
<>
<Portal>
<Modal
visible={show}
contentContainerStyle={styles.modal}
onDismiss={() => setShow(false)}>
<Text style={styles.title}>Add a set</Text>
<TextInput
style={styles.text}
autoFocus
placeholder="Name *"
label="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 *"
style={styles.text}
label="Reps *"
keyboardType="numeric"
value={reps}
onChangeText={setReps}
ref={repsRef}
onSubmitEditing={() => unitRef.current?.focus()}
onSubmitEditing={() => weightRef.current?.focus()}
/>
<TextInput
placeholder="Unit (kg)"
style={styles.text}
label="Weight *"
keyboardType="numeric"
value={weight}
onChangeText={setWeight}
onSubmitEditing={save}
ref={weightRef}
/>
<TextInput
style={styles.text}
label="Unit (kg)"
value={unit}
onChangeText={setUnit}
ref={unitRef}
@ -115,9 +118,11 @@ export default function EditSet({
Delete
</Button>
</View>
</View>
</Modal>
</Modal>
</Portal>
<Button
icon="add"
mode="contained"
onPress={() => {
setId(undefined);
@ -125,28 +130,24 @@ export default function EditSet({
}}>
Add
</Button>
</View>
</>
);
}
const styles = StyleSheet.create({
modal: {
height: '100%',
backgroundColor: 'black',
padding: 20,
},
text: {
marginBottom: 10,
},
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,
marginBottom: 10,
},
});

View File

@ -1,15 +1,61 @@
import {NativeStackScreenProps} from '@react-navigation/native-stack';
import {Button, Text, View} from 'react-native';
import React, {useEffect, useState} from 'react';
import {FlatList, StyleSheet, View} from 'react-native';
import {List, TextInput} from 'react-native-paper';
import {RootStackParamList} from './App';
import React from 'react';
import {getDb} from './db';
import Exercise from './exercise';
export default function Exercises({
navigation,
}: NativeStackScreenProps<RootStackParamList, 'Exercises'>) {
const [exercises, setExercises] = useState<Exercise[]>([]);
const [refreshing, setRefreshing] = useState(false);
const [search, setSearch] = useState('');
const refresh = async () => {
setRefreshing(true);
const db = await getDb();
const [result] = await db.executeSql(
`SELECT name, reps, unit, MAX(weight) AS weight
FROM sets
WHERE name LIKE ?
GROUP BY name;`,
[`%${search}%`],
);
setRefreshing(false);
if (!result) return setExercises([]);
setExercises(result.rows.raw());
};
useEffect(() => navigation.addListener('focus', refresh), [navigation]);
useEffect(() => {
refresh();
}, [search]);
const renderItem = ({item}: {item: Exercise}) => (
<List.Item
key={item.name}
title={item.name}
description={`Best: ${item.reps} x ${item.weight}${item.unit}`}
/>
);
return (
<View>
<Text>Pull ups - 1 rep</Text>
<Button title="Go home" onPress={() => navigation.navigate('Home', {})} />
<View style={styles.container}>
<TextInput label="Search" value={search} onChangeText={setSearch} />
<FlatList
onRefresh={refresh}
refreshing={refreshing}
renderItem={renderItem}
data={exercises}
/>
</View>
);
}
const styles = StyleSheet.create({
container: {
padding: 10,
},
});

View File

@ -1,42 +1,38 @@
import AsyncStorage from '@react-native-async-storage/async-storage';
import {NativeStackScreenProps} from '@react-navigation/native-stack';
import {useNavigation} from '@react-navigation/native';
import React, {useEffect, useState} from 'react';
import {
FlatList,
NativeModules,
SafeAreaView,
StyleSheet,
TextInput,
Vibration,
View,
} from 'react-native';
import {Button, List} from 'react-native-paper';
import {List, TextInput} from 'react-native-paper';
import Sound from 'react-native-sound';
import Alarm from './Alarm';
import {RootStackParamList} from './App';
import {getDb} from './db';
import EditSet from './EditSet';
import Set from './Set';
import Set from './set';
const limit = 20;
export default function Home({
navigation,
}: NativeStackScreenProps<RootStackParamList, 'Home'>) {
export default function 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 navigation = useNavigation();
const refresh = async () => {
setRefreshing(true);
const db = await getDb();
const [result] = await db.executeSql(
`SELECT * from sets WHERE name LIKE ? LIMIT ? OFFSET ?`,
`SELECT * from sets WHERE name LIKE ? ORDER BY created DESC LIMIT ? OFFSET ?`,
[`%${search}%`, limit, 0],
);
setRefreshing(false);
@ -75,7 +71,6 @@ export default function Home({
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');
@ -86,12 +81,6 @@ export default function Home({
await AsyncStorage.setItem('nextAlarm', when.toISOString());
};
const close = () => {
alarm.stop();
Vibration.cancel();
setShowTimer(false);
};
const next = async () => {
const newOffset = offset + limit;
setRefreshing(true);
@ -109,7 +98,7 @@ export default function Home({
return (
<SafeAreaView style={styles.container}>
<TextInput placeholder="Search" value={search} onChangeText={setSearch} />
<TextInput label="Search" value={search} onChangeText={setSearch} />
<FlatList
style={{height: '100%'}}
data={sets}
@ -120,28 +109,15 @@ export default function Home({
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>
<Alarm />
<EditSet
id={id}
setId={setId}
show={showEdit}
setShow={setShowEdit}
onSave={save}
/>
</View>
{showTimer && <Alarm onClose={close} />}
</SafeAreaView>
);
}
@ -150,25 +126,12 @@ const styles = StyleSheet.create({
name: {
fontSize: 18,
},
button: {
marginRight: 10,
},
container: {
flex: 1,
paddingLeft: 20,
paddingRight: 20,
padding: 10,
},
bottom: {
alignSelf: 'center',
marginBottom: 10,
flexDirection: 'row',
},
set: {
marginBottom: 10,
fontSize: 18,
shadowColor: 'red',
shadowRadius: 10,
shadowOffset: {width: 2, height: 40},
shadowOpacity: 8,
},
});

View File

@ -1,8 +1,8 @@
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 {StyleSheet, Text, View} from 'react-native';
import {Button, Switch, TextInput} from 'react-native-paper';
import {RootStackParamList} from './App';
import {getDb} from './db';
@ -34,27 +34,27 @@ export default function Settings({
return (
<View style={styles.container}>
<Text>Rest minutes</Text>
<TextInput
label="Rest minutes"
value={minutes}
keyboardType="numeric"
placeholder="3"
onChangeText={setMinutes}
/>
<Text>Rest seconds</Text>
<TextInput
label="Rest seconds"
value={seconds}
keyboardType="numeric"
placeholder="30"
onChangeText={setSeconds}
/>
<Text>Alarm enabled?</Text>
<Text style={{marginTop: 10, marginBottom: 10}}>Alarm enabled?</Text>
<Switch
style={{alignSelf: 'flex-start'}}
value={alarmEnabled}
onValueChange={setAlarmEnabled}
/>
<Button icon="trash" onPress={clear}>
<Button style={{alignSelf: 'flex-start'}} icon="trash" onPress={clear}>
Clear sets
</Button>
</View>
@ -63,7 +63,6 @@ export default function Settings({
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 10,
},
});

6
exercise.ts Normal file
View File

@ -0,0 +1,6 @@
export default interface Exercise {
name: string;
reps: number;
weight: number;
unit: string;
}

View File

@ -21,10 +21,6 @@ export default function Main() {
AppRegistry.registerComponent(appName, () => Main);
PushNotification.configure({
onRegister: function (token) {
console.log('TOKEN:', token);
},
onNotification: function (notification) {
console.log('NOTIFICATION:', notification);
},

View File