Move all database operations into db.ts

This commit is contained in:
Brandon Presley 2022-09-04 15:28:21 +12:00
parent 607f83955d
commit 259d36d67f
21 changed files with 444 additions and 337 deletions

View File

@ -6,39 +6,21 @@ import {
import React, {useCallback, useEffect, useState} from 'react'; import React, {useCallback, useEffect, useState} from 'react';
import {FlatList, StyleSheet, View} from 'react-native'; import {FlatList, StyleSheet, View} from 'react-native';
import {List, Searchbar} from 'react-native-paper'; import {List, Searchbar} from 'react-native-paper';
import Best from './best';
import {BestPageParams} from './BestPage'; import {BestPageParams} from './BestPage';
import {db} from './db'; import {getBestReps, getBestWeights} from './db';
import Set from './set';
export default function BestList() { export default function BestList() {
const [bests, setBests] = useState<Best[]>([]); const [bests, setBests] = useState<Set[]>([]);
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
const [refreshing, setRefresing] = useState(false); const [refreshing, setRefresing] = useState(false);
const navigation = useNavigation<NavigationProp<BestPageParams>>(); const navigation = useNavigation<NavigationProp<BestPageParams>>();
const refresh = useCallback(async () => { const refresh = useCallback(async () => {
const bestWeight = ` const weights = await getBestWeights(search);
SELECT name, reps, unit, MAX(weight) AS weight let newBest: Set[] = [];
FROM sets for (const set of weights)
WHERE name LIKE ? AND NOT hidden newBest.push(...(await getBestReps(search, set.weight)));
GROUP BY name;
`;
const bestReps = `
SELECT name, MAX(reps) as reps, unit, weight
FROM sets
WHERE name = ? AND weight = ? AND NOT hidden
GROUP BY name;
`;
const [weight] = await db.executeSql(bestWeight, [`%${search}%`]);
if (!weight) return setBests([]);
let newBest: Best[] = [];
for (let i = 0; i < weight.rows.length; i++) {
const [reps] = await db.executeSql(bestReps, [
weight.rows.item(i).name,
weight.rows.item(i).weight,
]);
newBest = newBest.concat(reps.rows.raw());
}
setBests(newBest); setBests(newBest);
}, [search]); }, [search]);
@ -55,7 +37,7 @@ export default function BestList() {
refresh(); refresh();
}, [search, refresh]); }, [search, refresh]);
const renderItem = ({item}: {item: Best}) => ( const renderItem = ({item}: {item: Set}) => (
<List.Item <List.Item
key={item.name} key={item.name}
title={item.name} title={item.name}

View File

@ -4,15 +4,15 @@ import {createStackNavigator} from '@react-navigation/stack';
import React from 'react'; import React from 'react';
import {IconButton} from 'react-native-paper'; import {IconButton} from 'react-native-paper';
import {DrawerParamList} from './App'; import {DrawerParamList} from './App';
import Best from './best';
import BestList from './BestList'; import BestList from './BestList';
import Set from './set';
import ViewBest from './ViewBest'; import ViewBest from './ViewBest';
const Stack = createStackNavigator<BestPageParams>(); const Stack = createStackNavigator<BestPageParams>();
export type BestPageParams = { export type BestPageParams = {
BestList: {}; BestList: {};
ViewBest: { ViewBest: {
best: Best; best: Set;
}; };
}; };

View File

@ -5,9 +5,15 @@ import {FileSystem} from 'react-native-file-access';
import {Divider, IconButton, Menu} from 'react-native-paper'; import {Divider, IconButton, Menu} from 'react-native-paper';
import {DrawerParamList, SnackbarContext} from './App'; import {DrawerParamList, SnackbarContext} from './App';
import ConfirmDialog from './ConfirmDialog'; import ConfirmDialog from './ConfirmDialog';
import {db} from './db'; import {
addPlans,
addSets,
deletePlans,
deleteSets,
getAllPlans,
getAllSets,
} from './db';
import {Plan} from './plan'; import {Plan} from './plan';
import Set from './set';
import {write} from './write'; import {write} from './write';
const setFields = 'id,name,reps,weight,created,unit,hidden'; const setFields = 'id,name,reps,weight,created,unit,hidden';
@ -20,9 +26,7 @@ export default function DrawerMenu({name}: {name: keyof DrawerParamList}) {
const {reset} = useNavigation<NavigationProp<DrawerParamList>>(); const {reset} = useNavigation<NavigationProp<DrawerParamList>>();
const exportSets = useCallback(async () => { const exportSets = useCallback(async () => {
const [result] = await db.executeSql('SELECT * FROM sets'); const sets = await getAllSets();
if (result.rows.length === 0) return;
const sets: Set[] = result.rows.raw();
const data = [setFields] const data = [setFields]
.concat( .concat(
sets.map( sets.map(
@ -36,13 +40,11 @@ export default function DrawerMenu({name}: {name: keyof DrawerParamList}) {
}, []); }, []);
const exportPlans = useCallback(async () => { const exportPlans = useCallback(async () => {
const [result] = await db.executeSql('SELECT * FROM plans'); const plans: Plan[] = await getAllPlans();
if (result.rows.length === 0) return;
const sets: Plan[] = result.rows.raw();
const data = [planFields] const data = [planFields]
.concat(sets.map(set => `"${set.id}","${set.days}","${set.workouts}"`)) .concat(plans.map(set => `"${set.id}","${set.days}","${set.workouts}"`))
.join('\n'); .join('\n');
console.log(`${DrawerMenu.name}.exportPlans`, {length: sets.length}); console.log(`${DrawerMenu.name}.exportPlans`, {length: plans.length});
await write('plans.csv', data); await write('plans.csv', data);
}, []); }, []);
@ -66,12 +68,10 @@ export default function DrawerMenu({name}: {name: keyof DrawerParamList}) {
return `('${cells[1]}',${cells[2]},${cells[3]},'${cells[4]}','${cells[5]}',${cells[6]})`; return `('${cells[1]}',${cells[2]},${cells[3]},'${cells[4]}','${cells[5]}',${cells[6]})`;
}) })
.join(','); .join(',');
await db.executeSql( await addSets(values);
`INSERT INTO sets(name,reps,weight,created,unit,hidden) VALUES ${values}`,
);
toast('Data imported.', 3000); toast('Data imported.', 3000);
reset({index: 0, routes: [{name}]}); reset({index: 0, routes: [{name}]});
}, [db, reset, name, toast]); }, [reset, name, toast]);
const uploadPlans = useCallback(async () => { const uploadPlans = useCallback(async () => {
const result = await DocumentPicker.pickSingle(); const result = await DocumentPicker.pickSingle();
@ -88,9 +88,9 @@ export default function DrawerMenu({name}: {name: keyof DrawerParamList}) {
return `('${cells[1]}','${cells[2]}')`; return `('${cells[1]}','${cells[2]}')`;
}) })
.join(','); .join(',');
await db.executeSql(`INSERT INTO plans(days,workouts) VALUES ${values}`); await addPlans(values);
toast('Data imported.', 3000); toast('Data imported.', 3000);
}, [db, toast]); }, [toast]);
const upload = useCallback(async () => { const upload = useCallback(async () => {
setShowMenu(false); setShowMenu(false);
@ -102,11 +102,11 @@ export default function DrawerMenu({name}: {name: keyof DrawerParamList}) {
const remove = useCallback(async () => { const remove = useCallback(async () => {
setShowMenu(false); setShowMenu(false);
setShowRemove(false); setShowRemove(false);
if (name === 'Home') await db.executeSql(`DELETE FROM sets`); if (name === 'Home') await deleteSets();
else if (name === 'Plans') await db.executeSql(`DELETE FROM plans`); else if (name === 'Plans') await deletePlans();
toast('All data has been deleted.', 4000); toast('All data has been deleted.', 4000);
reset({index: 0, routes: [{name}]}); reset({index: 0, routes: [{name}]});
}, [db, reset, name, toast]); }, [reset, name, toast]);
if (name === 'Home' || name === 'Plans') if (name === 'Home' || name === 'Plans')
return ( return (

View File

@ -9,7 +9,7 @@ import React, {useCallback, useEffect, useState} from 'react';
import {ScrollView, StyleSheet, Text, View} from 'react-native'; import {ScrollView, StyleSheet, Text, View} from 'react-native';
import {Button, IconButton} from 'react-native-paper'; import {Button, IconButton} from 'react-native-paper';
import {DrawerParamList} from './App'; import {DrawerParamList} from './App';
import {db} from './db'; import {addPlan, getNames, setPlan} from './db';
import MassiveSwitch from './MassiveSwitch'; import MassiveSwitch from './MassiveSwitch';
import {PlanPageParams} from './PlanPage'; import {PlanPageParams} from './PlanPage';
import {DAYS} from './time'; import {DAYS} from './time';
@ -36,14 +36,7 @@ export default function EditPlan() {
); );
useEffect(() => { useEffect(() => {
const refresh = async () => { getNames().then(setNames);
const [namesResult] = await db.executeSql(
'SELECT DISTINCT name FROM sets',
);
if (!namesResult.rows.length) return setNames([]);
setNames(namesResult.rows.raw().map(({name}) => name));
};
refresh();
}, []); }, []);
const save = useCallback(async () => { const save = useCallback(async () => {
@ -51,16 +44,9 @@ export default function EditPlan() {
if (!days || !workouts) return; if (!days || !workouts) return;
const newWorkouts = workouts.filter(workout => workout).join(','); const newWorkouts = workouts.filter(workout => workout).join(',');
const newDays = days.filter(day => day).join(','); const newDays = days.filter(day => day).join(',');
if (!params.plan.id) if (!params.plan.id) await addPlan({days: newDays, workouts: newWorkouts});
await db.executeSql(`INSERT INTO plans(days, workouts) VALUES (?, ?)`, [
newDays,
newWorkouts,
]);
else else
await db.executeSql( await setPlan({days: newDays, workouts: newWorkouts, id: params.plan.id});
`UPDATE plans SET days = ?, workouts = ? WHERE id = ?`,
[newDays, newWorkouts, params.plan.id],
);
navigation.goBack(); navigation.goBack();
}, [days, workouts, params, navigation]); }, [days, workouts, params, navigation]);

View File

@ -8,11 +8,10 @@ import React, {useCallback, useContext} from 'react';
import {NativeModules, View} from 'react-native'; import {NativeModules, View} from 'react-native';
import {IconButton} from 'react-native-paper'; import {IconButton} from 'react-native-paper';
import {SnackbarContext} from './App'; import {SnackbarContext} from './App';
import {db} from './db'; import {addSet, getSettings, setSet} from './db';
import {HomePageParams} from './HomePage'; import {HomePageParams} from './HomePage';
import Set from './set'; import Set from './set';
import SetForm from './SetForm'; import SetForm from './SetForm';
import Settings from './settings';
export default function EditSet() { export default function EditSet() {
const {params} = useRoute<RouteProp<HomePageParams, 'EditSet'>>(); const {params} = useRoute<RouteProp<HomePageParams, 'EditSet'>>();
@ -32,8 +31,7 @@ export default function EditSet() {
); );
const startTimer = useCallback(async () => { const startTimer = useCallback(async () => {
const [result] = await db.executeSql(`SELECT * FROM settings LIMIT 1`); const settings = await getSettings();
const settings: Settings = result.rows.item(0);
if (!settings.alarm) return; if (!settings.alarm) return;
const milliseconds = settings.minutes * 60 * 1000 + settings.seconds * 1000; const milliseconds = settings.minutes * 60 * 1000 + settings.seconds * 1000;
NativeModules.AlarmModule.timer( NativeModules.AlarmModule.timer(
@ -46,10 +44,7 @@ export default function EditSet() {
const update = useCallback( const update = useCallback(
async (set: Set) => { async (set: Set) => {
console.log(`${EditSet.name}.update`, set); console.log(`${EditSet.name}.update`, set);
await db.executeSql( await setSet(set);
`UPDATE sets SET name = ?, reps = ?, weight = ?, unit = ? WHERE id = ?`,
[set.name, set.reps, set.weight, set.unit, set.id],
);
navigation.goBack(); navigation.goBack();
}, },
[navigation], [navigation],
@ -57,19 +52,13 @@ export default function EditSet() {
const add = useCallback( const add = useCallback(
async (set: Set) => { async (set: Set) => {
const {name, reps, weight, unit, image} = set;
const insert = `
INSERT INTO sets(name, reps, weight, created, unit, image)
VALUES (?,?,?,strftime('%Y-%m-%dT%H:%M:%S', 'now', 'localtime'),?, ?)
`;
startTimer(); startTimer();
await db.executeSql(insert, [name, reps, weight, unit, image]); await addSet(set);
const [result] = await db.executeSql(`SELECT * FROM settings LIMIT 1`); const settings = await getSettings();
const settings: Settings = result.rows.item(0);
if (settings.notify === 0) return navigation.goBack(); if (settings.notify === 0) return navigation.goBack();
if ( if (
weight > params.set.weight || set.weight > params.set.weight ||
(reps > params.set.reps && weight === params.set.weight) (set.reps > params.set.reps && set.weight === params.set.weight)
) )
toast("Great work King, that's a new record!", 3000); toast("Great work King, that's a new record!", 3000);
navigation.goBack(); navigation.goBack();

View File

@ -9,13 +9,14 @@ import {Image, ScrollView, View} from 'react-native';
import DocumentPicker from 'react-native-document-picker'; import DocumentPicker from 'react-native-document-picker';
import {Button, IconButton} from 'react-native-paper'; import {Button, IconButton} from 'react-native-paper';
import {set} from 'react-native-reanimated'; import {set} from 'react-native-reanimated';
import {db} from './db'; import {addSet, getSets, setSetImage, setSetName, setWorkouts} from './db';
import MassiveInput from './MassiveInput'; import MassiveInput from './MassiveInput';
import Set from './set';
import {WorkoutsPageParams} from './WorkoutsPage'; import {WorkoutsPageParams} from './WorkoutsPage';
export default function EditWorkout() { export default function EditWorkout() {
const [name, setName] = useState(''); const [name, setName] = useState('');
const [uri, setUri] = useState(''); const [uri, setUri] = useState<string>();
const {params} = useRoute<RouteProp<WorkoutsPageParams, 'EditWorkout'>>(); const {params} = useRoute<RouteProp<WorkoutsPageParams, 'EditWorkout'>>();
const navigation = useNavigation(); const navigation = useNavigation();
@ -28,39 +29,24 @@ export default function EditWorkout() {
headerRight: null, headerRight: null,
title: params.value.name ? params.value.name : 'New workout', title: params.value.name ? params.value.name : 'New workout',
}); });
db.executeSql(`SELECT image FROM sets WHERE name = ? LIMIT 1`, [ getSets({search: params.value.name, limit: 1, offset: 0}).then(sets =>
params.value.name, setUri(sets[0]?.image),
]).then(([result]) => setUri(result.rows.item(0)?.image)); );
}, [navigation, params.value.name]), }, [navigation, params.value.name]),
); );
const update = useCallback(async () => { const update = useCallback(async () => {
console.log(`${EditWorkout.name}.update`, set); console.log(`${EditWorkout.name}.update`, set);
if (name) { if (name) {
await db.executeSql(`UPDATE sets SET name = ? WHERE name = ?`, [ await setSetName(params.value.name, name);
name, await setWorkouts(params.value.name, name);
params.value.name,
]);
await db.executeSql(
`UPDATE plans SET workouts = REPLACE(workouts, ?, ?)
WHERE workouts LIKE ?`,
[params.value.name, name, `%${params.value.name}%`],
);
} }
if (uri) if (uri) await setSetImage(params.value.name, uri);
await db.executeSql(`UPDATE sets SET image = ? WHERE name = ?`, [
uri,
params.value.name,
]);
navigation.goBack(); navigation.goBack();
}, [navigation, params.value.name, name, uri]); }, [navigation, params.value.name, name, uri]);
const add = useCallback(async () => { const add = useCallback(async () => {
const insert = ` await addSet({name, reps: 0, weight: 0, hidden: true} as Set);
INSERT INTO sets(name, reps, weight, created, unit, hidden)
VALUES (?,0,0,strftime('%Y-%m-%dT%H:%M:%S', 'now', 'localtime'),'kg',true)
`;
await db.executeSql(insert, [name]);
navigation.goBack(); navigation.goBack();
}, [navigation, name]); }, [navigation, name]);

View File

@ -2,7 +2,7 @@ import {NavigationProp, useNavigation} from '@react-navigation/native';
import React, {useCallback, useState} from 'react'; import React, {useCallback, useState} from 'react';
import {GestureResponderEvent} from 'react-native'; import {GestureResponderEvent} from 'react-native';
import {List, Menu} from 'react-native-paper'; import {List, Menu} from 'react-native-paper';
import {db} from './db'; import {deletePlan} from './db';
import {Plan} from './plan'; import {Plan} from './plan';
import {PlanPageParams} from './PlanPage'; import {PlanPageParams} from './PlanPage';
@ -18,7 +18,7 @@ export default function PlanItem({
const navigation = useNavigation<NavigationProp<PlanPageParams>>(); const navigation = useNavigation<NavigationProp<PlanPageParams>>();
const remove = useCallback(async () => { const remove = useCallback(async () => {
await db.executeSql(`DELETE FROM plans WHERE id = ?`, [item.id]); if (item.id) await deletePlan(item.id);
setShow(false); setShow(false);
onRemove(); onRemove();
}, [setShow, item.id, onRemove]); }, [setShow, item.id, onRemove]);

View File

@ -6,7 +6,7 @@ import {
import React, {useCallback, useEffect, useState} from 'react'; import React, {useCallback, useEffect, useState} from 'react';
import {FlatList, StyleSheet, View} from 'react-native'; import {FlatList, StyleSheet, View} from 'react-native';
import {List, Searchbar} from 'react-native-paper'; import {List, Searchbar} from 'react-native-paper';
import {db} from './db'; import {getPlans} from './db';
import DrawerMenu from './DrawerMenu'; import DrawerMenu from './DrawerMenu';
import MassiveFab from './MassiveFab'; import MassiveFab from './MassiveFab';
import {Plan} from './plan'; import {Plan} from './plan';
@ -20,14 +20,7 @@ export default function PlanList() {
const navigation = useNavigation<NavigationProp<PlanPageParams>>(); const navigation = useNavigation<NavigationProp<PlanPageParams>>();
const refresh = useCallback(async () => { const refresh = useCallback(async () => {
const selectPlans = ` getPlans(search).then(setPlans);
SELECT * from plans
WHERE days LIKE ? OR workouts LIKE ?
`;
const getPlans = ({s}: {s: string}) =>
db.executeSql(selectPlans, [`%${s}%`, `%${s}%`]);
const [plansResult] = await getPlans({s: search});
setPlans(plansResult.rows.raw());
}, [search]); }, [search]);
useFocusEffect( useFocusEffect(
@ -57,7 +50,7 @@ export default function PlanList() {
style={{height: '100%'}} style={{height: '100%'}}
data={plans} data={plans}
renderItem={renderItem} renderItem={renderItem}
keyExtractor={set => set.id.toString()} keyExtractor={set => set.id?.toString() || ''}
refreshing={refreshing} refreshing={refreshing}
onRefresh={() => { onRefresh={() => {
setRefresing(true); setRefresing(true);

View File

@ -1,7 +1,7 @@
import React, {useEffect, useRef, useState} from 'react'; import React, {useEffect, useRef, useState} from 'react';
import {ScrollView} from 'react-native'; import {ScrollView} from 'react-native';
import {Button, Text} from 'react-native-paper'; import {Button, Text} from 'react-native-paper';
import {db} from './db'; import {getSets} from './db';
import MassiveInput from './MassiveInput'; import MassiveInput from './MassiveInput';
import Set from './set'; import Set from './set';
@ -29,9 +29,9 @@ export default function SetForm({
useEffect(() => { useEffect(() => {
console.log('SetForm.useEffect:', {uri, name: set.name}); console.log('SetForm.useEffect:', {uri, name: set.name});
if (!uri) if (!uri)
db.executeSql(`SELECT image FROM sets WHERE name = ? LIMIT 1`, [ getSets({search: set.name, limit: 1, offset: 0}).then(sets =>
set.name, setUri(sets[0]?.image),
]).then(([result]) => setUri(result.rows.item(0)?.image)); );
}, [uri, set.name]); }, [uri, set.name]);
const handleSubmit = () => { const handleSubmit = () => {

View File

@ -2,7 +2,7 @@ import {NavigationProp, useNavigation} from '@react-navigation/native';
import React, {useCallback, useState} from 'react'; import React, {useCallback, useState} from 'react';
import {GestureResponderEvent, Image} from 'react-native'; import {GestureResponderEvent, Image} from 'react-native';
import {Divider, List, Menu, Text} from 'react-native-paper'; import {Divider, List, Menu, Text} from 'react-native-paper';
import {db} from './db'; import {deleteSet} from './db';
import {HomePageParams} from './HomePage'; import {HomePageParams} from './HomePage';
import Set from './set'; import Set from './set';
@ -26,7 +26,7 @@ export default function SetItem({
const navigation = useNavigation<NavigationProp<HomePageParams>>(); const navigation = useNavigation<NavigationProp<HomePageParams>>();
const remove = useCallback(async () => { const remove = useCallback(async () => {
await db.executeSql(`DELETE FROM sets WHERE id = ?`, [item.id]); await deleteSet(item.id);
setShowMenu(false); setShowMenu(false);
onRemove(); onRemove();
}, [setShowMenu, onRemove, item.id]); }, [setShowMenu, onRemove, item.id]);

View File

@ -6,24 +6,21 @@ import {
import React, {useCallback, useEffect, useState} from 'react'; import React, {useCallback, useEffect, useState} from 'react';
import {FlatList, StyleSheet, View} from 'react-native'; import {FlatList, StyleSheet, View} from 'react-native';
import {List, Searchbar} from 'react-native-paper'; import {List, Searchbar} from 'react-native-paper';
import {db} from './db'; import {
defaultSet,
getBest,
getSets,
getSettings,
getTodaysPlan,
getTodaysSets,
} from './db';
import DrawerMenu from './DrawerMenu'; import DrawerMenu from './DrawerMenu';
import {HomePageParams} from './HomePage'; import {HomePageParams} from './HomePage';
import MassiveFab from './MassiveFab'; import MassiveFab from './MassiveFab';
import {Plan} from './plan';
import Set from './set'; import Set from './set';
import SetItem from './SetItem'; import SetItem from './SetItem';
import Settings from './settings';
import {DAYS} from './time';
const limit = 15; const limit = 15;
const defaultSet = {
name: '',
id: 0,
reps: 10,
weight: 20,
unit: 'kg',
};
export default function SetList() { export default function SetList() {
const [sets, setSets] = useState<Set[]>(); const [sets, setSets] = useState<Set[]>();
@ -37,21 +34,13 @@ export default function SetList() {
const [images, setImages] = useState(true); const [images, setImages] = useState(true);
const navigation = useNavigation<NavigationProp<HomePageParams>>(); const navigation = useNavigation<NavigationProp<HomePageParams>>();
const selectSets = `
SELECT * from sets
WHERE name LIKE ? AND NOT hidden
ORDER BY created DESC
LIMIT ? OFFSET ?
`;
const refresh = useCallback(async () => { const refresh = useCallback(async () => {
const [result] = await db.executeSql(selectSets, [`%${search}%`, limit, 0]); const newSets = await getSets({search: `%${search}%`, limit, offset: 0});
if (!result) return setSets([]); if (newSets.length === 0) return setSets([]);
console.log(`${SetList.name}.refresh:`, {search, limit}); setSets(newSets);
setSets(result.rows.raw());
setOffset(0); setOffset(0);
setEnd(false); setEnd(false);
}, [search, selectSets]); }, [search]);
const refreshLoader = useCallback(async () => { const refreshLoader = useCallback(async () => {
setRefreshing(true); setRefreshing(true);
@ -62,52 +51,8 @@ export default function SetList() {
refresh(); refresh();
}, [search, refresh]); }, [search, refresh]);
const getTodaysPlan = useCallback(async (): Promise<Plan[]> => {
const today = DAYS[new Date().getDay()];
const [result] = await db.executeSql(
`SELECT * FROM plans WHERE days LIKE ? LIMIT 1`,
[`%${today}%`],
);
return result.rows.raw();
}, [db]);
const getTodaysSets = useCallback(async (): Promise<Set[]> => {
const today = new Date().toISOString().split('T')[0];
const [result] = await db.executeSql(
`SELECT * FROM sets WHERE created LIKE ? ORDER BY created DESC`,
[`${today}%`],
);
return result.rows.raw();
}, [db]);
const getBest = useCallback(
async (query: string): Promise<Set> => {
const bestWeight = `
SELECT name, reps, unit, MAX(weight) AS weight
FROM sets
WHERE name = ? AND NOT hidden
GROUP BY name;
`;
const bestReps = `
SELECT name, MAX(reps) as reps, unit, weight
FROM sets
WHERE name = ? AND weight = ? AND NOT hidden
GROUP BY name;
`;
const [weightResult] = await db.executeSql(bestWeight, [query]);
if (!weightResult.rows.length) return {...defaultSet};
const [repsResult] = await db.executeSql(bestReps, [
query,
weightResult.rows.item(0).weight,
]);
return repsResult.rows.item(0);
},
[db],
);
const predict = useCallback(async () => { const predict = useCallback(async () => {
const [result] = await db.executeSql(`SELECT * FROM settings LIMIT 1`); const settings = await getSettings();
const settings: Settings = result.rows.item(0);
if (!settings.predict) return; if (!settings.predict) return;
const todaysPlan = await getTodaysPlan(); const todaysPlan = await getTodaysPlan();
console.log(`${SetList.name}.predict:`, {todaysPlan}); console.log(`${SetList.name}.predict:`, {todaysPlan});
@ -130,7 +75,7 @@ export default function SetList() {
console.log(`${SetList.name}.predict:`, {best}); console.log(`${SetList.name}.predict:`, {best});
setSet({...best}); setSet({...best});
setWorkouts(todaysWorkouts); setWorkouts(todaysWorkouts);
}, [getTodaysSets, getTodaysPlan, getBest, db]); }, []);
useFocusEffect( useFocusEffect(
useCallback(() => { useCallback(() => {
@ -139,11 +84,8 @@ export default function SetList() {
navigation.getParent()?.setOptions({ navigation.getParent()?.setOptions({
headerRight: () => <DrawerMenu name="Home" />, headerRight: () => <DrawerMenu name="Home" />,
}); });
db.executeSql('SELECT * FROM settings LIMIT 1').then(([result]) => { getSettings().then(settings => setImages(!!settings.images));
const settings: Settings = result.rows.item(0); }, [refresh, predict, navigation]),
setImages(!!settings.images);
});
}, [refresh, predict, navigation, db]),
); );
const renderItem = useCallback( const renderItem = useCallback(
@ -171,15 +113,17 @@ export default function SetList() {
newOffset, newOffset,
search, search,
}); });
const [result] = await db const newSets = await getSets({
.executeSql(selectSets, [`%${search}%`, limit, newOffset]) search: `%${search}%`,
.finally(() => setRefreshing(false)); limit,
if (result.rows.length === 0) return setEnd(true); offset: newOffset,
});
if (newSets.length === 0) return setEnd(true);
if (!sets) return; if (!sets) return;
setSets([...sets, ...result.rows.raw()]); setSets([...sets, ...newSets]);
if (result.rows.length < limit) return setEnd(true); if (newSets.length < limit) return setEnd(true);
setOffset(newOffset); setOffset(newOffset);
}, [search, end, offset, sets, db, selectSets]); }, [search, end, offset, sets]);
const onAdd = useCallback(async () => { const onAdd = useCallback(async () => {
navigation.navigate('EditSet', { navigation.navigate('EditSet', {

View File

@ -10,15 +10,14 @@ import DocumentPicker from 'react-native-document-picker';
import {Button, Searchbar, Text} from 'react-native-paper'; import {Button, Searchbar, Text} from 'react-native-paper';
import {SnackbarContext} from './App'; import {SnackbarContext} from './App';
import ConfirmDialog from './ConfirmDialog'; import ConfirmDialog from './ConfirmDialog';
import {db} from './db'; import {getSettings, setSettings} from './db';
import MassiveInput from './MassiveInput'; import MassiveInput from './MassiveInput';
import MassiveSwitch from './MassiveSwitch'; import MassiveSwitch from './MassiveSwitch';
import Settings from './settings';
export default function SettingsPage() { export default function SettingsPage() {
const [vibrate, setVibrate] = useState(true); const [vibrate, setVibrate] = useState(true);
const [minutes, setMinutes] = useState<string>(''); const [minutes, setMinutes] = useState<string>('');
const [maxSets, setMaxSets] = useState<string>('3'); const [sets, setMaxSets] = useState<string>('3');
const [seconds, setSeconds] = useState<string>(''); const [seconds, setSeconds] = useState<string>('');
const [alarm, setAlarm] = useState(false); const [alarm, setAlarm] = useState(false);
const [predict, setPredict] = useState(false); const [predict, setPredict] = useState(false);
@ -31,8 +30,7 @@ export default function SettingsPage() {
const {toast} = useContext(SnackbarContext); const {toast} = useContext(SnackbarContext);
const refresh = useCallback(async () => { const refresh = useCallback(async () => {
const [result] = await db.executeSql(`SELECT * FROM settings LIMIT 1`); const settings = await getSettings();
const settings: Settings = result.rows.item(0);
console.log('SettingsPage.refresh:', {settings}); console.log('SettingsPage.refresh:', {settings});
setMinutes(settings.minutes.toString()); setMinutes(settings.minutes.toString());
setSeconds(settings.seconds.toString()); setSeconds(settings.seconds.toString());
@ -51,31 +49,18 @@ export default function SettingsPage() {
}, [refresh]); }, [refresh]);
useEffect(() => { useEffect(() => {
db.executeSql( setSettings({
`UPDATE settings SET vibrate=?,minutes=?,sets=?,seconds=?,alarm=?,predict=?,sound=?,notify=?,images=?`, vibrate: +vibrate,
[ minutes: +minutes,
vibrate, seconds: +seconds,
minutes, alarm: +alarm,
maxSets, predict: +predict,
seconds, sound,
alarm, notify: +notify,
predict, images: +images,
sound, sets: +sets,
notify, });
images, }, [vibrate, minutes, sets, seconds, alarm, predict, sound, notify, images]);
],
);
}, [
vibrate,
minutes,
maxSets,
seconds,
alarm,
predict,
sound,
notify,
images,
]);
const changeAlarmEnabled = useCallback( const changeAlarmEnabled = useCallback(
(enabled: boolean) => { (enabled: boolean) => {
@ -124,7 +109,7 @@ export default function SettingsPage() {
element: ( element: (
<MassiveInput <MassiveInput
label="Sets per workout" label="Sets per workout"
value={maxSets} value={sets}
keyboardType="numeric" keyboardType="numeric"
onChangeText={value => { onChangeText={value => {
setMaxSets(value); setMaxSets(value);

View File

@ -12,27 +12,12 @@ import Share from 'react-native-share';
import ViewShot from 'react-native-view-shot'; import ViewShot from 'react-native-view-shot';
import {BestPageParams} from './BestPage'; import {BestPageParams} from './BestPage';
import Chart from './Chart'; import Chart from './Chart';
import {db} from './db'; import {getVolumes, getWeights} from './db';
import {Metrics} from './metrics';
import {Periods} from './periods';
import Set from './set'; import Set from './set';
import {formatMonth} from './time'; import {formatMonth} from './time';
import Volume from './volume';
interface Volume {
name: string;
created: string;
value: number;
unit: string;
}
enum Metrics {
Weight = 'Best weight per day',
Volume = 'Volume per day',
}
enum Periods {
Weekly = 'This week',
Monthly = 'This month',
Yearly = 'This year',
}
export default function ViewBest() { export default function ViewBest() {
const {params} = useRoute<RouteProp<BestPageParams, 'ViewBest'>>(); const {params} = useRoute<RouteProp<BestPageParams, 'ViewBest'>>();
@ -72,51 +57,14 @@ export default function ViewBest() {
}, [navigation, params.best]), }, [navigation, params.best]),
); );
const refreshWeight = useCallback(async () => {
const select = `
SELECT max(weight) AS weight,
STRFTIME('%Y-%m-%d', created) as created, unit
FROM sets
WHERE name = ? AND NOT hidden
AND DATE(created) >= DATE('now', 'weekday 0', ?)
GROUP BY name, STRFTIME('%Y-%m-%d', created)
`;
let difference = '-7 days';
if (period === Periods.Monthly) difference = '-1 months';
else if (period === Periods.Yearly) difference = '-1 years';
const [result] = await db.executeSql(select, [
params.best.name,
difference,
]);
if (result.rows.length === 0) return;
setWeights(result.rows.raw());
}, [params.best.name, period]);
const refreshVolume = useCallback(async () => {
const select = `
SELECT sum(weight * reps) AS value,
STRFTIME('%Y-%m-%d', created) as created, unit
FROM sets
WHERE name = ? AND NOT hidden
AND DATE(created) >= DATE('now', 'weekday 0', ?)
GROUP BY name, STRFTIME('%Y-%m-%d', created)
`;
let difference = '-7 days';
if (period === Periods.Monthly) difference = '-1 months';
else if (period === Periods.Yearly) difference = '-1 years';
const [result] = await db.executeSql(select, [
params.best.name,
difference,
]);
if (result.rows.length === 0) return;
setVolumes(result.rows.raw());
}, [params.best.name, period]);
useEffect(() => { useEffect(() => {
if (metric === Metrics.Weight) refreshWeight(); if (metric === Metrics.Weight)
else if (metric === Metrics.Volume) refreshVolume(); getWeights(params.best.name, period).then(setWeights);
else if (metric === Metrics.Volume)
getVolumes(params.best.name, period).then(setVolumes);
console.log(`${ViewBest.name}.useEffect`, {metric, period}); console.log(`${ViewBest.name}.useEffect`, {metric, period});
}, [params.best.name, metric, period, refreshVolume, refreshWeight]); }, [params.best.name, metric, period]);
return ( return (
<ViewShot style={{padding: 10}} ref={viewShot}> <ViewShot style={{padding: 10}} ref={viewShot}>

View File

@ -3,7 +3,7 @@ import React, {useCallback, useEffect, useState} from 'react';
import {GestureResponderEvent, Image} from 'react-native'; import {GestureResponderEvent, Image} from 'react-native';
import {List, Menu, Text} from 'react-native-paper'; import {List, Menu, Text} from 'react-native-paper';
import ConfirmDialog from './ConfirmDialog'; import ConfirmDialog from './ConfirmDialog';
import {db} from './db'; import {deleteSetsBy, getSets} from './db';
import Workout from './workout'; import Workout from './workout';
import {WorkoutsPageParams} from './WorkoutsPage'; import {WorkoutsPageParams} from './WorkoutsPage';
@ -17,20 +17,17 @@ export default function WorkoutItem({
const [showMenu, setShowMenu] = useState(false); const [showMenu, setShowMenu] = useState(false);
const [anchor, setAnchor] = useState({x: 0, y: 0}); const [anchor, setAnchor] = useState({x: 0, y: 0});
const [showRemove, setShowRemove] = useState(''); const [showRemove, setShowRemove] = useState('');
const [uri, setUri] = useState(''); const [uri, setUri] = useState<string>();
const navigation = useNavigation<NavigationProp<WorkoutsPageParams>>(); const navigation = useNavigation<NavigationProp<WorkoutsPageParams>>();
useEffect(() => { useEffect(() => {
db.executeSql(`SELECT image FROM sets WHERE name = ? LIMIT 1`, [ getSets({search: item.name, limit: 1, offset: 0}).then(sets =>
item.name, setUri(sets[0]?.image),
]).then(([result]) => { );
setUri(result.rows.item(0)?.image);
console.log(WorkoutItem.name, item.name, result.rows.item(0)?.image);
});
}, [item.name]); }, [item.name]);
const remove = useCallback(async () => { const remove = useCallback(async () => {
await db.executeSql(`DELETE FROM sets WHERE name = ?`, [item.name]); await deleteSetsBy(item.name);
setShowMenu(false); setShowMenu(false);
onRemoved(); onRemoved();
}, [setShowMenu, onRemoved, item.name]); }, [setShowMenu, onRemoved, item.name]);

View File

@ -6,7 +6,7 @@ import {
import React, {useCallback, useEffect, useState} from 'react'; import React, {useCallback, useEffect, useState} from 'react';
import {FlatList, StyleSheet, View} from 'react-native'; import {FlatList, StyleSheet, View} from 'react-native';
import {List, Searchbar} from 'react-native-paper'; import {List, Searchbar} from 'react-native-paper';
import {db} from './db'; import {getWorkouts} from './db';
import MassiveFab from './MassiveFab'; import MassiveFab from './MassiveFab';
import SetList from './SetList'; import SetList from './SetList';
import Workout from './workout'; import Workout from './workout';
@ -23,22 +23,16 @@ export default function WorkoutList() {
const [end, setEnd] = useState(false); const [end, setEnd] = useState(false);
const navigation = useNavigation<NavigationProp<WorkoutsPageParams>>(); const navigation = useNavigation<NavigationProp<WorkoutsPageParams>>();
const select = `
SELECT DISTINCT sets.name
FROM sets
WHERE sets.name LIKE ?
ORDER BY sets.name
LIMIT ? OFFSET ?
`;
const refresh = useCallback(async () => { const refresh = useCallback(async () => {
const [result] = await db.executeSql(select, [`%${search}%`, limit, 0]); const newWorkouts = await getWorkouts({
if (!result) return setWorkouts([]); search: `%${search}%`,
console.log(`${WorkoutList.name}.refresh:`, {search, limit}); limit,
setWorkouts(result.rows.raw()); offset: 0,
});
setWorkouts(newWorkouts);
setOffset(0); setOffset(0);
setEnd(false); setEnd(false);
}, [search, select]); }, [search]);
const refreshLoader = useCallback(async () => { const refreshLoader = useCallback(async () => {
setRefreshing(true); setRefreshing(true);
@ -72,15 +66,17 @@ export default function WorkoutList() {
newOffset, newOffset,
search, search,
}); });
const [result] = await db const newWorkouts = await getWorkouts({
.executeSql(select, [`%${search}%`, limit, newOffset]) search: `%${search}%`,
.finally(() => setRefreshing(false)); limit,
if (result.rows.length === 0) return setEnd(true); offset: newOffset,
}).finally(() => setRefreshing(false));
if (newWorkouts.length === 0) return setEnd(true);
if (!workouts) return; if (!workouts) return;
setWorkouts([...workouts, ...result.rows.raw()]); setWorkouts([...workouts, ...newWorkouts]);
if (result.rows.length < limit) return setEnd(true); if (newWorkouts.length < limit) return setEnd(true);
setOffset(newOffset); setOffset(newOffset);
}, [search, end, offset, workouts, select]); }, [search, end, offset, workouts]);
const onAdd = useCallback(async () => { const onAdd = useCallback(async () => {
navigation.navigate('EditWorkout', { navigation.navigate('EditWorkout', {

View File

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

294
db.ts
View File

@ -3,6 +3,13 @@ import {
openDatabase, openDatabase,
SQLiteDatabase, SQLiteDatabase,
} from 'react-native-sqlite-storage'; } from 'react-native-sqlite-storage';
import {Periods} from './periods';
import {Plan} from './plan';
import Set from './set';
import Settings from './settings';
import {DAYS} from './time';
import Volume from './volume';
import Workout from './workout';
enablePromise(true); enablePromise(true);
@ -86,5 +93,290 @@ export const migrations = async () => {
await db.executeSql(addImages).catch(() => null); await db.executeSql(addImages).catch(() => null);
const [result] = await db.executeSql(selectSettings); const [result] = await db.executeSql(selectSettings);
if (result.rows.length === 0) await db.executeSql(insertSettings); if (result.rows.length === 0) await db.executeSql(insertSettings);
return db; };
export const getSettings = async () => {
const [result] = await db.executeSql(`SELECT * FROM settings LIMIT 1`);
const settings: Settings = result.rows.item(0);
return settings;
};
export const setSettings = async (value: Settings) => {
const update = `
UPDATE settings
SET vibrate=?,minutes=?,sets=?,seconds=?,alarm=?,
predict=?,sound=?,notify=?,images=?
`;
return db.executeSql(update, [
value.vibrate,
value.minutes,
value.sets,
value.seconds,
value.alarm,
value.predict,
value.sound,
value.notify,
value.images,
]);
};
export const setSet = async (value: Set) => {
const update = `
UPDATE sets
SET name = ?, reps = ?, weight = ?, unit = ?
WHERE id = ?
`;
return db.executeSql(update, [
value.name,
value.reps,
value.weight,
value.unit,
value.id,
]);
};
export const addSets = async (values: string) => {
const insert = `
INSERT INTO sets(name,reps,weight,created,unit,hidden)
VALUES ${values}
`;
return db.executeSql(insert);
};
export const addSet = async (value: Set) => {
const insert = `
INSERT INTO sets(name, reps, weight, created, unit, image)
VALUES (?,?,?,strftime('%Y-%m-%dT%H:%M:%S', 'now', 'localtime'),?, ?)
`;
const {name, reps, weight, unit, image} = value;
return db.executeSql(insert, [name, reps, weight, unit, image]);
};
export const setWorkouts = async (oldName: string, newName: string) => {
const update = `
UPDATE plans SET workouts = REPLACE(workouts, ?, ?)
WHERE workouts LIKE ?
`;
return db.executeSql(update, [oldName, newName, `%${oldName}%`]);
};
export const setPlan = async (value: Plan) => {
const update = `UPDATE plans SET days = ?, workouts = ? WHERE id = ?`;
return db.executeSql(update, [value.days, value.workouts, value.id]);
};
export const addPlan = async (value: Plan) => {
const insert = `INSERT INTO plans(days, workouts) VALUES (?, ?)`;
return db.executeSql(insert, [value.days, value.workouts]);
};
export const addPlans = async (values: string) => {
const insert = `
INSERT INTO plans(days,workouts) VALUES ${values}
`;
return db.executeSql(insert);
};
export const deletePlans = async () => {
return db.executeSql(`DELETE FROM plans`);
};
export const deleteSets = async () => {
return db.executeSql(`DELETE FROM sets`);
};
export const deletePlan = async (id: number) => {
return db.executeSql(`DELETE FROM plans WHERE id = ?`, [id]);
};
export const deleteSet = async (id: number) => {
return db.executeSql(`DELETE FROM sets WHERE id = ?`, [id]);
};
export const deleteSetsBy = async (name: string) => {
return db.executeSql(`DELETE FROM sets WHERE name = ?`, [name]);
};
export const getAllPlans = async (): Promise<Plan[]> => {
const select = `SELECT * from plans`;
const [result] = await db.executeSql(select);
return result.rows.raw();
};
export const getAllSets = async (): Promise<Set[]> => {
const select = `SELECT * from sets`;
const [result] = await db.executeSql(select);
return result.rows.raw();
};
export interface PageParams {
search: string;
limit: number;
offset: number;
}
export const getSets = async ({
search,
limit,
offset,
}: PageParams): Promise<Set[]> => {
const select = `
SELECT * from sets
WHERE name LIKE ? AND NOT hidden
ORDER BY created DESC
LIMIT ? OFFSET ?
`;
const [result] = await db.executeSql(select, [`%${search}%`, limit, offset]);
return result.rows.raw();
};
export const getTodaysPlan = async (): Promise<Plan[]> => {
const today = DAYS[new Date().getDay()];
const [result] = await db.executeSql(
`SELECT * FROM plans WHERE days LIKE ? LIMIT 1`,
[`%${today}%`],
);
return result.rows.raw();
};
export const getTodaysSets = async (): Promise<Set[]> => {
const today = new Date().toISOString().split('T')[0];
const [result] = await db.executeSql(
`SELECT * FROM sets WHERE created LIKE ? ORDER BY created DESC`,
[`${today}%`],
);
return result.rows.raw();
};
export const defaultSet = {
name: '',
id: 0,
reps: 10,
weight: 20,
unit: 'kg',
};
export const getBest = async (query: string): Promise<Set> => {
const bestWeight = `
SELECT name, reps, unit, MAX(weight) AS weight
FROM sets
WHERE name = ? AND NOT hidden
GROUP BY name;
`;
const bestReps = `
SELECT name, MAX(reps) as reps, unit, weight
FROM sets
WHERE name = ? AND weight = ? AND NOT hidden
GROUP BY name;
`;
const [weightResult] = await db.executeSql(bestWeight, [query]);
if (!weightResult.rows.length) return {...defaultSet};
const [repsResult] = await db.executeSql(bestReps, [
query,
weightResult.rows.item(0).weight,
]);
return repsResult.rows.item(0);
};
export const setSetName = async (oldName: string, newName: string) => {
const update = `UPDATE sets SET name = ? WHERE name = ?`;
return db.executeSql(update, [newName, oldName]);
};
export const setSetImage = async (name: string, image: string) => {
const update = `UPDATE sets SET image = ? WHERE name = ?`;
return db.executeSql(update, [name, image]);
};
export const getWeights = async (
name: string,
period: Periods,
): Promise<Set[]> => {
const select = `
SELECT max(weight) AS weight,
STRFTIME('%Y-%m-%d', created) as created, unit
FROM sets
WHERE name = ? AND NOT hidden
AND DATE(created) >= DATE('now', 'weekday 0', ?)
GROUP BY name, STRFTIME('%Y-%m-%d', created)
`;
let difference = '-7 days';
if (period === Periods.Monthly) difference = '-1 months';
else if (period === Periods.Yearly) difference = '-1 years';
const [result] = await db.executeSql(select, [name, difference]);
return result.rows.raw();
};
export const getVolumes = async (
name: string,
period: Periods,
): Promise<Volume[]> => {
const select = `
SELECT sum(weight * reps) AS value,
STRFTIME('%Y-%m-%d', created) as created, unit
FROM sets
WHERE name = ? AND NOT hidden
AND DATE(created) >= DATE('now', 'weekday 0', ?)
GROUP BY name, STRFTIME('%Y-%m-%d', created)
`;
let difference = '-7 days';
if (period === Periods.Monthly) difference = '-1 months';
else if (period === Periods.Yearly) difference = '-1 years';
const [result] = await db.executeSql(select, [name, difference]);
return result.rows.raw();
};
export const getNames = async (): Promise<string[]> => {
const [result] = await db.executeSql('SELECT DISTINCT name FROM sets');
return result.rows.raw();
};
export const getBestWeights = async (search: string): Promise<Set[]> => {
const select = `
SELECT name, reps, unit, MAX(weight) AS weight
FROM sets
WHERE name LIKE ? AND NOT hidden
GROUP BY name;
`;
const [result] = await db.executeSql(select, [`%${search}%`]);
return result.rows.raw();
};
export const getBestReps = async (
name: string,
weight: number,
): Promise<Set[]> => {
const select = `
SELECT name, MAX(reps) as reps, unit, weight
FROM sets
WHERE name = ? AND weight = ? AND NOT hidden
GROUP BY name;
`;
const [result] = await db.executeSql(select, [name, weight]);
return result.rows.raw();
};
export const getPlans = async (search: string): Promise<Plan[]> => {
const select = `
SELECT * from plans
WHERE days LIKE ? OR workouts LIKE ?
`;
const [result] = await db.executeSql(select, [`%${search}%`, `%${search}%`]);
return result.rows.raw();
};
export const getWorkouts = async ({
search,
limit,
offset,
}: PageParams): Promise<Workout[]> => {
const select = `
SELECT DISTINCT sets.name
FROM sets
WHERE sets.name LIKE ?
ORDER BY sets.name
LIMIT ? OFFSET ?
`;
const [result] = await db.executeSql(select, [search, limit, offset]);
return result.rows.raw();
}; };

4
metrics.ts Normal file
View File

@ -0,0 +1,4 @@
export enum Metrics {
Weight = 'Best weight per day',
Volume = 'Volume per day',
}

5
periods.ts Normal file
View File

@ -0,0 +1,5 @@
export enum Periods {
Weekly = 'This week',
Monthly = 'This month',
Yearly = 'This year',
}

View File

@ -1,5 +1,5 @@
export interface Plan { export interface Plan {
id: number; id?: number;
days: string; days: string;
workouts: string; workouts: string;
} }

6
volume.ts Normal file
View File

@ -0,0 +1,6 @@
export default interface Volume {
name: string;
created: string;
value: number;
unit: string;
}