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

View File

@ -1,10 +1,9 @@
import React, {useEffect, useState} from 'react';
import {StyleSheet, Text, View} from 'react-native';
import {Button, Dialog, Modal, Portal, TextInput} from 'react-native-paper';
import React, {useContext, useEffect, useState} from 'react';
import {Button, Dialog, Portal} from 'react-native-paper';
import {DatabaseContext} from './App';
import DayMenu from './DayMenu';
import WorkoutMenu from './WorkoutMenu';
import {getDb} from './db';
import {Plan} from './plan';
import WorkoutMenu from './WorkoutMenu';
export default function EditPlan({
id,
@ -22,28 +21,28 @@ export default function EditPlan({
const [days, setDays] = useState('');
const [workouts, setWorkouts] = useState('');
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(() => {
getDb().then(async db => {
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);
});
refresh();
}, [id]);
const save = async () => {
if (!days || !workouts) return;
const db = await getDb();
if (!id)
await db.executeSql(`INSERT INTO plans(days, workouts) VALUES (?, ?)`, [
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 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({
id,
onSave,
show,
setShow,
clearId,
set,
}: {
id?: number;
clearId: () => void;
onSave: () => void;
show: boolean;
setShow: (visible: boolean) => void;
set?: Set;
}) {
const [name, setName] = useState('');
const [reps, setReps] = useState('');
@ -26,27 +24,24 @@ export default function EditSet({
const weightRef = useRef<any>(null);
const repsRef = 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(() => {
if (!id) return setCreated(new Date(new Date().toUTCString()));
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);
setCreated(new Date(set.created));
});
}, [id]);
refresh();
}, [set]);
const save = async () => {
if (!name || !reps || !weight) return;
const db = await getDb();
if (!id)
if (!set)
await db.executeSql(
`INSERT INTO sets(name, reps, weight, created, unit) VALUES (?,?,?,?,?)`,
[name, reps, weight, new Date().toISOString(), unit || 'kg'],
@ -54,7 +49,7 @@ export default function EditSet({
else
await db.executeSql(
`UPDATE sets SET name = ?, reps = ?, weight = ?, unit = ? WHERE id = ?`,
[name, reps, weight, unit, id],
[name, reps, weight, unit, set.id],
);
setShow(false);
onSave();
@ -63,7 +58,7 @@ export default function EditSet({
return (
<Portal>
<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>
<TextInput
style={styles.text}

View File

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

View File

@ -1,9 +1,9 @@
import AsyncStorage from '@react-native-async-storage/async-storage';
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 {AnimatedFAB, Searchbar} from 'react-native-paper';
import {getSets} from './db';
import {DatabaseContext} from './App';
import EditSet from './EditSet';
import Set from './set';
@ -13,11 +13,28 @@ const limit = 20;
export default function Home() {
const [sets, setSets] = useState<Set[]>();
const [id, setId] = useState<number>();
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 [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 [result] = await getSets({search, limit, offset: 0});
@ -43,8 +60,8 @@ export default function Home() {
<SetItem
item={item}
key={item.id}
setShowEdit={setShowEdit}
setId={setId}
setShowEdit={setShow}
setSet={setEdit}
onRemove={refresh}
/>
);
@ -82,13 +99,7 @@ export default function Home() {
refreshing={refreshing}
onRefresh={refreshLoader}
/>
<EditSet
clearId={() => setId(undefined)}
id={id}
show={showEdit}
setShow={setShowEdit}
onSave={save}
/>
<EditSet set={edit} show={show} setShow={setShow} onSave={save} />
<AnimatedFAB
extended={false}
@ -96,8 +107,8 @@ export default function Home() {
icon="add"
style={{position: 'absolute', right: 20, bottom: 20}}
onPress={() => {
setId(undefined);
setShowEdit(true);
setEdit(undefined);
setShow(true);
}}
/>
</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 {getDb} from './db';
import {DatabaseContext} from './App';
import {Plan} from './plan';
export default function PlanItem({
@ -15,9 +15,9 @@ export default function PlanItem({
onRemove: () => void;
}) {
const [show, setShow] = useState(false);
const db = useContext(DatabaseContext);
const remove = async () => {
const db = await getDb();
await db.executeSql(`DELETE FROM plans WHERE id = ?`, [item.id]);
setShow(false);
onRemove();

View File

@ -1,9 +1,9 @@
import {useFocusEffect} from '@react-navigation/native';
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 {AnimatedFAB, ProgressBar, Searchbar} from 'react-native-paper';
import {getPlans, getProgress} from './db';
import {DatabaseContext} from './App';
import EditPlan from './EditPlan';
import {Plan} from './plan';
import PlanItem from './PlanItem';
@ -18,6 +18,22 @@ export default function Plans() {
const [progresses, setProgresses] = useState<Progress[]>([]);
const today = `%${format(new Date(new Date().toUTCString()), 'EEEE')}%`;
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 [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 {getDb} from './db';
import {DatabaseContext} from './App';
import Set from './set';
export default function SetItem({
item,
setId,
setSet,
setShowEdit,
onRemove,
}: {
item: Set;
setId: (id: number) => void;
setSet: (set: Set) => void;
setShowEdit: (show: boolean) => void;
onRemove: () => void;
}) {
const [show, setShow] = useState(false);
const db = useContext(DatabaseContext);
const remove = async () => {
const db = await getDb();
await db.executeSql(`DELETE FROM sets WHERE id = ?`, [item.id]);
setShow(false);
onRemove();
@ -27,7 +27,7 @@ export default function SetItem({
<>
<List.Item
onPress={() => {
setId(item.id);
setSet(item);
setShowEdit(true);
}}
title={item.name}

View File

@ -1,10 +1,10 @@
import AsyncStorage from '@react-native-async-storage/async-storage';
import {useFocusEffect, useNavigation} from '@react-navigation/native';
import React, {useEffect, useState} from 'react';
import {useFocusEffect} from '@react-navigation/native';
import React, {useContext, useEffect, useState} from 'react';
import {NativeModules, StyleSheet, Text, View} from 'react-native';
import {Button, Snackbar, Switch, TextInput} from 'react-native-paper';
import {DatabaseContext} from './App';
import BatteryDialog from './BatteryDialog';
import {getDb} from './db';
export default function Settings() {
const [minutes, setMinutes] = useState<string>('');
@ -13,6 +13,7 @@ export default function Settings() {
const [snackbar, setSnackbar] = useState('');
const [showBattery, setShowBattery] = useState(false);
const [ignoring, setIgnoring] = useState(false);
const db = useContext(DatabaseContext);
const refresh = async () => {
setMinutes((await AsyncStorage.getItem('minutes')) || '3');
@ -34,7 +35,6 @@ export default function Settings() {
const clear = async () => {
setSnackbar('Deleting all data...');
setTimeout(() => setSnackbar(''), 5000);
const db = await getDb();
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);
export const getDb = () => openDatabase({name: 'massive.db'});
const createSets = `
export const createSets = `
CREATE TABLE IF NOT EXISTS sets (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
@ -14,7 +14,7 @@ const createSets = `
);
`;
const createPlans = `
export const createPlans = `
CREATE TABLE IF NOT EXISTS plans (
id INTEGER PRIMARY KEY AUTOINCREMENT,
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 = `
SELECT count(*) as count from sets
WHERE created LIKE ?
@ -44,21 +29,3 @@ const selectProgress = `
`;
export const getProgress = ({created, name}: {created: string; name: string}) =>
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]));