Ensure only one connection to SQLite exists

This commit is contained in:
Brandon Presley 2022-07-07 14:18:38 +12:00
parent ecb436f8a6
commit 570b43715f
10 changed files with 127 additions and 131 deletions

34
App.tsx
View File

@ -1,19 +1,20 @@
import {useAsyncStorage} from '@react-native-async-storage/async-storage'; import {useAsyncStorage} from '@react-native-async-storage/async-storage';
import Ionicon from 'react-native-vector-icons/Ionicons';
import {createMaterialTopTabNavigator} from '@react-navigation/material-top-tabs'; import {createMaterialTopTabNavigator} from '@react-navigation/material-top-tabs';
import { import {
DarkTheme, DarkTheme,
DefaultTheme, DefaultTheme,
NavigationContainer, NavigationContainer,
} from '@react-navigation/native'; } from '@react-navigation/native';
import React, {useEffect} from 'react'; import React, {useEffect, useState} from 'react';
import {StatusBar, useColorScheme} from 'react-native'; import {StatusBar, useColorScheme} from 'react-native';
import { import {
DarkTheme as DarkThemePaper, DarkTheme as DarkThemePaper,
DefaultTheme as DefaultThemePaper, DefaultTheme as DefaultThemePaper,
Provider, Provider,
} from 'react-native-paper'; } from 'react-native-paper';
import {setupSchema} from './db'; import {SQLiteDatabase} from 'react-native-sqlite-storage';
import Ionicon from 'react-native-vector-icons/Ionicons';
import {createPlans, createSets, getDb} from './db';
import Exercises from './Exercises'; import Exercises from './Exercises';
import Home from './Home'; import Home from './Home';
import Plans from './Plans'; import Plans from './Plans';
@ -27,16 +28,21 @@ export type RootStackParamList = {
Plans: {}; Plans: {};
}; };
setupSchema(); export const DatabaseContext = React.createContext<SQLiteDatabase>({} as any);
const App = () => { const App = () => {
const [db, setDb] = useState<SQLiteDatabase | null>(null);
const dark = useColorScheme() === 'dark'; const dark = useColorScheme() === 'dark';
const {getItem: getMinutes, setItem: setMinutes} = useAsyncStorage('minutes'); const {getItem: getMinutes, setItem: setMinutes} = useAsyncStorage('minutes');
const {getItem: getSeconds, setItem: setSeconds} = useAsyncStorage('seconds'); const {getItem: getSeconds, setItem: setSeconds} = useAsyncStorage('seconds');
const {getItem: getAlarmEnabled, setItem: setAlarmEnabled} = const {getItem: getAlarmEnabled, setItem: setAlarmEnabled} =
useAsyncStorage('alarmEnabled'); useAsyncStorage('alarmEnabled');
const defaults = async () => { const init = async () => {
const gotDb = await getDb();
await gotDb.executeSql(createPlans);
await gotDb.executeSql(createSets);
setDb(gotDb);
const minutes = await getMinutes(); const minutes = await getMinutes();
if (minutes === null) await setMinutes('3'); if (minutes === null) await setMinutes('3');
const seconds = await getSeconds(); const seconds = await getSeconds();
@ -46,7 +52,7 @@ const App = () => {
}; };
useEffect(() => { useEffect(() => {
defaults(); init();
}, []); }, []);
return ( return (
@ -55,12 +61,16 @@ const App = () => {
settings={{icon: props => <Ionicon {...props} />}}> settings={{icon: props => <Ionicon {...props} />}}>
<NavigationContainer theme={dark ? DarkTheme : DefaultTheme}> <NavigationContainer theme={dark ? DarkTheme : DefaultTheme}>
<StatusBar barStyle={dark ? 'light-content' : 'dark-content'} /> <StatusBar barStyle={dark ? 'light-content' : 'dark-content'} />
<Tab.Navigator> {db && (
<Tab.Screen name="Home" component={Home} /> <DatabaseContext.Provider value={db}>
<Tab.Screen name="Plans" component={Plans} /> <Tab.Navigator>
<Tab.Screen name="Exercises" component={Exercises} /> <Tab.Screen name="Home" component={Home} />
<Tab.Screen name="Settings" component={Settings} /> <Tab.Screen name="Plans" component={Plans} />
</Tab.Navigator> <Tab.Screen name="Exercises" component={Exercises} />
<Tab.Screen name="Settings" component={Settings} />
</Tab.Navigator>
</DatabaseContext.Provider>
)}
</NavigationContainer> </NavigationContainer>
</Provider> </Provider>
); );

View File

@ -1,10 +1,9 @@
import React, {useEffect, useState} from 'react'; import React, {useContext, useEffect, useState} from 'react';
import {StyleSheet, Text, View} from 'react-native'; import {Button, Dialog, Portal} from 'react-native-paper';
import {Button, Dialog, Modal, Portal, TextInput} from 'react-native-paper'; import {DatabaseContext} from './App';
import DayMenu from './DayMenu'; import DayMenu from './DayMenu';
import WorkoutMenu from './WorkoutMenu';
import {getDb} from './db';
import {Plan} from './plan'; import {Plan} from './plan';
import WorkoutMenu from './WorkoutMenu';
export default function EditPlan({ export default function EditPlan({
id, id,
@ -22,28 +21,28 @@ export default function EditPlan({
const [days, setDays] = useState(''); const [days, setDays] = useState('');
const [workouts, setWorkouts] = useState(''); const [workouts, setWorkouts] = useState('');
const [names, setNames] = useState<string[]>([]); const [names, setNames] = useState<string[]>([]);
const db = useContext(DatabaseContext);
const refresh = async () => {
const [namesResult] = await db.executeSql('SELECT DISTINCT name FROM sets');
if (!namesResult.rows.length) return;
setNames(namesResult.rows.raw().map(({name}) => name));
if (!id) return;
const [result] = await db.executeSql(`SELECT * FROM plans WHERE id = ?`, [
id,
]);
if (!result.rows.item(0)) throw new Error("Can't find specified Set.");
const set: Plan = result.rows.item(0);
setDays(set.days);
setWorkouts(set.workouts);
};
useEffect(() => { useEffect(() => {
getDb().then(async db => { refresh();
const [namesResult] = await db.executeSql(
'SELECT DISTINCT name FROM sets',
);
if (!namesResult.rows.length) return;
setNames(namesResult.rows.raw().map(({name}) => name));
if (!id) return;
const [result] = await db.executeSql(`SELECT * FROM plans WHERE id = ?`, [
id,
]);
if (!result.rows.item(0)) throw new Error("Can't find specified Set.");
const set: Plan = result.rows.item(0);
setDays(set.days);
setWorkouts(set.workouts);
});
}, [id]); }, [id]);
const save = async () => { const save = async () => {
if (!days || !workouts) return; if (!days || !workouts) return;
const db = await getDb();
if (!id) if (!id)
await db.executeSql(`INSERT INTO plans(days, workouts) VALUES (?, ?)`, [ await db.executeSql(`INSERT INTO plans(days, workouts) VALUES (?, ?)`, [
days, days,

View File

@ -1,22 +1,20 @@
import React, {useEffect, useRef, useState} from 'react';
import {StyleSheet, Text, View} from 'react-native';
import {Button, Dialog, Modal, Portal, TextInput} from 'react-native-paper';
import {getDb} from './db';
import Set from './set';
import {format} from 'date-fns'; import {format} from 'date-fns';
import React, {useContext, useEffect, useRef, useState} from 'react';
import {StyleSheet, Text} from 'react-native';
import {Button, Dialog, Portal, TextInput} from 'react-native-paper';
import {DatabaseContext} from './App';
import Set from './set';
export default function EditSet({ export default function EditSet({
id,
onSave, onSave,
show, show,
setShow, setShow,
clearId, set,
}: { }: {
id?: number;
clearId: () => void;
onSave: () => void; onSave: () => void;
show: boolean; show: boolean;
setShow: (visible: boolean) => void; setShow: (visible: boolean) => void;
set?: Set;
}) { }) {
const [name, setName] = useState(''); const [name, setName] = useState('');
const [reps, setReps] = useState(''); const [reps, setReps] = useState('');
@ -26,27 +24,24 @@ export default function EditSet({
const weightRef = useRef<any>(null); const weightRef = useRef<any>(null);
const repsRef = useRef<any>(null); const repsRef = useRef<any>(null);
const unitRef = useRef<any>(null); const unitRef = useRef<any>(null);
const db = useContext(DatabaseContext);
const refresh = async () => {
if (!set) return setCreated(new Date(new Date().toUTCString()));
setName(set.name);
setReps(set.reps.toString());
setWeight(set.weight.toString());
setUnit(set.unit);
setCreated(new Date(set.created));
};
useEffect(() => { useEffect(() => {
if (!id) return setCreated(new Date(new Date().toUTCString())); refresh();
getDb().then(async db => { }, [set]);
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);
setCreated(new Date(set.created));
});
}, [id]);
const save = async () => { const save = async () => {
if (!name || !reps || !weight) return; if (!name || !reps || !weight) return;
const db = await getDb(); if (!set)
if (!id)
await db.executeSql( await db.executeSql(
`INSERT INTO sets(name, reps, weight, created, unit) VALUES (?,?,?,?,?)`, `INSERT INTO sets(name, reps, weight, created, unit) VALUES (?,?,?,?,?)`,
[name, reps, weight, new Date().toISOString(), unit || 'kg'], [name, reps, weight, new Date().toISOString(), unit || 'kg'],
@ -54,7 +49,7 @@ export default function EditSet({
else else
await db.executeSql( await db.executeSql(
`UPDATE sets SET name = ?, reps = ?, weight = ?, unit = ? WHERE id = ?`, `UPDATE sets SET name = ?, reps = ?, weight = ?, unit = ? WHERE id = ?`,
[name, reps, weight, unit, id], [name, reps, weight, unit, set.id],
); );
setShow(false); setShow(false);
onSave(); onSave();
@ -63,7 +58,7 @@ export default function EditSet({
return ( return (
<Portal> <Portal>
<Dialog visible={show} onDismiss={() => setShow(false)}> <Dialog visible={show} onDismiss={() => setShow(false)}>
<Dialog.Title>{id ? `Edit "${name}"` : 'Add a set'}</Dialog.Title> <Dialog.Title>{set?.id ? `Edit "${name}"` : 'Add a set'}</Dialog.Title>
<Dialog.Content> <Dialog.Content>
<TextInput <TextInput
style={styles.text} style={styles.text}

View File

@ -1,19 +1,17 @@
import {useFocusEffect} from '@react-navigation/native'; import {useFocusEffect} from '@react-navigation/native';
import {NativeStackScreenProps} from '@react-navigation/native-stack'; import React, {useContext, useEffect, useState} from 'react';
import React, {useEffect, useState} from 'react';
import {FlatList, StyleSheet, View} from 'react-native'; import {FlatList, StyleSheet, View} from 'react-native';
import {List, Searchbar, TextInput} from 'react-native-paper'; import {List, Searchbar} from 'react-native-paper';
import {RootStackParamList} from './App'; import {DatabaseContext} from './App';
import {getDb} from './db';
import Exercise from './exercise'; import Exercise from './exercise';
export default function Exercises() { export default function Exercises() {
const [exercises, setExercises] = useState<Exercise[]>([]); const [exercises, setExercises] = useState<Exercise[]>([]);
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
const [refreshing, setRefresing] = useState(false); const [refreshing, setRefresing] = useState(false);
const db = useContext(DatabaseContext);
const refresh = async () => { const refresh = async () => {
const db = await getDb();
const [result] = await db.executeSql( const [result] = await db.executeSql(
`SELECT name, reps, unit, MAX(weight) AS weight `SELECT name, reps, unit, MAX(weight) AS weight
FROM sets FROM sets

View File

@ -1,9 +1,9 @@
import AsyncStorage from '@react-native-async-storage/async-storage'; import AsyncStorage from '@react-native-async-storage/async-storage';
import {useFocusEffect} from '@react-navigation/native'; import {useFocusEffect} from '@react-navigation/native';
import React, {useEffect, useState} from 'react'; import React, {useContext, useEffect, useState} from 'react';
import {FlatList, NativeModules, SafeAreaView, StyleSheet} from 'react-native'; import {FlatList, NativeModules, SafeAreaView, StyleSheet} from 'react-native';
import {AnimatedFAB, Searchbar} from 'react-native-paper'; import {AnimatedFAB, Searchbar} from 'react-native-paper';
import {getSets} from './db'; import {DatabaseContext} from './App';
import EditSet from './EditSet'; import EditSet from './EditSet';
import Set from './set'; import Set from './set';
@ -13,11 +13,28 @@ const limit = 20;
export default function Home() { export default function Home() {
const [sets, setSets] = useState<Set[]>(); const [sets, setSets] = useState<Set[]>();
const [id, setId] = useState<number>();
const [offset, setOffset] = useState(0); const [offset, setOffset] = useState(0);
const [showEdit, setShowEdit] = useState(false); const [edit, setEdit] = useState<Set>();
const [show, setShow] = useState(false);
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
const [refreshing, setRefresing] = useState(false); const [refreshing, setRefresing] = useState(false);
const db = useContext(DatabaseContext);
const selectSets = `
SELECT * from sets
WHERE name LIKE ?
ORDER BY created DESC
LIMIT ? OFFSET ?
`;
const getSets = ({
search,
limit,
offset,
}: {
search: string;
limit: number;
offset: number;
}) => db.executeSql(selectSets, [`%${search}%`, limit, offset]);
const refresh = async () => { const refresh = async () => {
const [result] = await getSets({search, limit, offset: 0}); const [result] = await getSets({search, limit, offset: 0});
@ -43,8 +60,8 @@ export default function Home() {
<SetItem <SetItem
item={item} item={item}
key={item.id} key={item.id}
setShowEdit={setShowEdit} setShowEdit={setShow}
setId={setId} setSet={setEdit}
onRemove={refresh} onRemove={refresh}
/> />
); );
@ -82,13 +99,7 @@ export default function Home() {
refreshing={refreshing} refreshing={refreshing}
onRefresh={refreshLoader} onRefresh={refreshLoader}
/> />
<EditSet <EditSet set={edit} show={show} setShow={setShow} onSave={save} />
clearId={() => setId(undefined)}
id={id}
show={showEdit}
setShow={setShowEdit}
onSave={save}
/>
<AnimatedFAB <AnimatedFAB
extended={false} extended={false}
@ -96,8 +107,8 @@ export default function Home() {
icon="add" icon="add"
style={{position: 'absolute', right: 20, bottom: 20}} style={{position: 'absolute', right: 20, bottom: 20}}
onPress={() => { onPress={() => {
setId(undefined); setEdit(undefined);
setShowEdit(true); setShow(true);
}} }}
/> />
</SafeAreaView> </SafeAreaView>

View File

@ -1,6 +1,6 @@
import React, {useState} from 'react'; import React, {useContext, useState} from 'react';
import {IconButton, List, Menu} from 'react-native-paper'; import {IconButton, List, Menu} from 'react-native-paper';
import {getDb} from './db'; import {DatabaseContext} from './App';
import {Plan} from './plan'; import {Plan} from './plan';
export default function PlanItem({ export default function PlanItem({
@ -15,9 +15,9 @@ export default function PlanItem({
onRemove: () => void; onRemove: () => void;
}) { }) {
const [show, setShow] = useState(false); const [show, setShow] = useState(false);
const db = useContext(DatabaseContext);
const remove = async () => { const remove = async () => {
const db = await getDb();
await db.executeSql(`DELETE FROM plans WHERE id = ?`, [item.id]); await db.executeSql(`DELETE FROM plans WHERE id = ?`, [item.id]);
setShow(false); setShow(false);
onRemove(); onRemove();

View File

@ -1,9 +1,9 @@
import {useFocusEffect} from '@react-navigation/native'; import {useFocusEffect} from '@react-navigation/native';
import {format} from 'date-fns'; import {format} from 'date-fns';
import React, {useEffect, useState} from 'react'; import React, {useContext, useEffect, useState} from 'react';
import {FlatList, StyleSheet, Text, View} from 'react-native'; import {FlatList, StyleSheet, Text, View} from 'react-native';
import {AnimatedFAB, ProgressBar, Searchbar} from 'react-native-paper'; import {AnimatedFAB, ProgressBar, Searchbar} from 'react-native-paper';
import {getPlans, getProgress} from './db'; import {DatabaseContext} from './App';
import EditPlan from './EditPlan'; import EditPlan from './EditPlan';
import {Plan} from './plan'; import {Plan} from './plan';
import PlanItem from './PlanItem'; import PlanItem from './PlanItem';
@ -18,6 +18,22 @@ export default function Plans() {
const [progresses, setProgresses] = useState<Progress[]>([]); const [progresses, setProgresses] = useState<Progress[]>([]);
const today = `%${format(new Date(new Date().toUTCString()), 'EEEE')}%`; const today = `%${format(new Date(new Date().toUTCString()), 'EEEE')}%`;
const now = `${format(new Date(new Date().toUTCString()), 'yyyy-MM-dd')}%`; const now = `${format(new Date(new Date().toUTCString()), 'yyyy-MM-dd')}%`;
const db = useContext(DatabaseContext);
const selectPlans = `
SELECT * from plans
WHERE days LIKE ? OR workouts LIKE ?
`;
const getPlans = ({search}: {search: string}) =>
db.executeSql(selectPlans, [`%${search}%`, `%${search}%`]);
const selectProgress = `
SELECT COUNT(*) as count from sets
WHERE created LIKE ?
AND name = ?
`;
const getProgress = ({created, name}: {created: string; name: string}) =>
db.executeSql(selectProgress, [`%${created}%`, name]);
const refresh = async () => { const refresh = async () => {
const [plansResult] = await getPlans({search}); const [plansResult] = await getPlans({search});

View File

@ -1,23 +1,23 @@
import React, {useState} from 'react'; import React, {useContext, useState} from 'react';
import {IconButton, List, Menu} from 'react-native-paper'; import {IconButton, List, Menu} from 'react-native-paper';
import {getDb} from './db'; import {DatabaseContext} from './App';
import Set from './set'; import Set from './set';
export default function SetItem({ export default function SetItem({
item, item,
setId, setSet,
setShowEdit, setShowEdit,
onRemove, onRemove,
}: { }: {
item: Set; item: Set;
setId: (id: number) => void; setSet: (set: Set) => void;
setShowEdit: (show: boolean) => void; setShowEdit: (show: boolean) => void;
onRemove: () => void; onRemove: () => void;
}) { }) {
const [show, setShow] = useState(false); const [show, setShow] = useState(false);
const db = useContext(DatabaseContext);
const remove = async () => { const remove = async () => {
const db = await getDb();
await db.executeSql(`DELETE FROM sets WHERE id = ?`, [item.id]); await db.executeSql(`DELETE FROM sets WHERE id = ?`, [item.id]);
setShow(false); setShow(false);
onRemove(); onRemove();
@ -27,7 +27,7 @@ export default function SetItem({
<> <>
<List.Item <List.Item
onPress={() => { onPress={() => {
setId(item.id); setSet(item);
setShowEdit(true); setShowEdit(true);
}} }}
title={item.name} title={item.name}

View File

@ -1,10 +1,10 @@
import AsyncStorage from '@react-native-async-storage/async-storage'; import AsyncStorage from '@react-native-async-storage/async-storage';
import {useFocusEffect, useNavigation} from '@react-navigation/native'; import {useFocusEffect} from '@react-navigation/native';
import React, {useEffect, useState} from 'react'; import React, {useContext, useEffect, useState} from 'react';
import {NativeModules, StyleSheet, Text, View} from 'react-native'; import {NativeModules, StyleSheet, Text, View} from 'react-native';
import {Button, Snackbar, Switch, TextInput} from 'react-native-paper'; import {Button, Snackbar, Switch, TextInput} from 'react-native-paper';
import {DatabaseContext} from './App';
import BatteryDialog from './BatteryDialog'; import BatteryDialog from './BatteryDialog';
import {getDb} from './db';
export default function Settings() { export default function Settings() {
const [minutes, setMinutes] = useState<string>(''); const [minutes, setMinutes] = useState<string>('');
@ -13,6 +13,7 @@ export default function Settings() {
const [snackbar, setSnackbar] = useState(''); const [snackbar, setSnackbar] = useState('');
const [showBattery, setShowBattery] = useState(false); const [showBattery, setShowBattery] = useState(false);
const [ignoring, setIgnoring] = useState(false); const [ignoring, setIgnoring] = useState(false);
const db = useContext(DatabaseContext);
const refresh = async () => { const refresh = async () => {
setMinutes((await AsyncStorage.getItem('minutes')) || '3'); setMinutes((await AsyncStorage.getItem('minutes')) || '3');
@ -34,7 +35,6 @@ export default function Settings() {
const clear = async () => { const clear = async () => {
setSnackbar('Deleting all data...'); setSnackbar('Deleting all data...');
setTimeout(() => setSnackbar(''), 5000); setTimeout(() => setSnackbar(''), 5000);
const db = await getDb();
await db.executeSql(`DELETE FROM sets`); await db.executeSql(`DELETE FROM sets`);
}; };

37
db.ts
View File

@ -3,7 +3,7 @@ import {enablePromise, openDatabase} from 'react-native-sqlite-storage';
enablePromise(true); enablePromise(true);
export const getDb = () => openDatabase({name: 'massive.db'}); export const getDb = () => openDatabase({name: 'massive.db'});
const createSets = ` export const createSets = `
CREATE TABLE IF NOT EXISTS sets ( CREATE TABLE IF NOT EXISTS sets (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL, name TEXT NOT NULL,
@ -14,7 +14,7 @@ const createSets = `
); );
`; `;
const createPlans = ` export const createPlans = `
CREATE TABLE IF NOT EXISTS plans ( CREATE TABLE IF NOT EXISTS plans (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
days TEXT NOT NULL, days TEXT NOT NULL,
@ -22,21 +22,6 @@ const createPlans = `
); );
`; `;
export const setupSchema = () =>
getDb().then(db => {
db.executeSql(createSets);
db.executeSql(createPlans);
});
const selectPlans = `
SELECT * from plans
WHERE days LIKE ? OR workouts LIKE ?
`;
export const getPlans = ({search}: {search: string}) =>
getDb().then(db =>
db.executeSql(selectPlans, [`%${search}%`, `%${search}%`]),
);
const selectProgress = ` const selectProgress = `
SELECT count(*) as count from sets SELECT count(*) as count from sets
WHERE created LIKE ? WHERE created LIKE ?
@ -44,21 +29,3 @@ const selectProgress = `
`; `;
export const getProgress = ({created, name}: {created: string; name: string}) => export const getProgress = ({created, name}: {created: string; name: string}) =>
getDb().then(db => db.executeSql(selectProgress, [`%${created}%`, name])); getDb().then(db => db.executeSql(selectProgress, [`%${created}%`, name]));
const selectSets = `
SELECT * from sets
WHERE name LIKE ?
ORDER BY created DESC
LIMIT ? OFFSET ?
`;
export const getSets = ({
search,
limit,
offset,
}: {
search: string;
limit: number;
offset: number;
}) =>
getDb().then(db => db.executeSql(selectSets, [`%${search}%`, limit, offset]));