From 570b43715f78e74106df757408f09962309cc35e Mon Sep 17 00:00:00 2001 From: Brandon Presley Date: Thu, 7 Jul 2022 14:18:38 +1200 Subject: [PATCH] Ensure only one connection to SQLite exists --- App.tsx | 34 ++++++++++++++++++++++------------ EditPlan.tsx | 41 ++++++++++++++++++++--------------------- EditSet.tsx | 49 ++++++++++++++++++++++--------------------------- Exercises.tsx | 10 ++++------ Home.tsx | 41 ++++++++++++++++++++++++++--------------- PlanItem.tsx | 6 +++--- Plans.tsx | 20 ++++++++++++++++++-- SetItem.tsx | 12 ++++++------ Settings.tsx | 8 ++++---- db.ts | 37 ++----------------------------------- 10 files changed, 127 insertions(+), 131 deletions(-) diff --git a/App.tsx b/App.tsx index e87a205..974d92c 100644 --- a/App.tsx +++ b/App.tsx @@ -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({} as any); const App = () => { + const [db, setDb] = useState(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 => }}> - - - - - - + {db && ( + + + + + + + + + )} ); diff --git a/EditPlan.tsx b/EditPlan.tsx index b0dad84..80c8501 100644 --- a/EditPlan.tsx +++ b/EditPlan.tsx @@ -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([]); + 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, diff --git a/EditSet.tsx b/EditSet.tsx index ecab368..2004e3e 100644 --- a/EditSet.tsx +++ b/EditSet.tsx @@ -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(null); const repsRef = useRef(null); const unitRef = useRef(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 ( setShow(false)}> - {id ? `Edit "${name}"` : 'Add a set'} + {set?.id ? `Edit "${name}"` : 'Add a set'} ([]); 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 diff --git a/Home.tsx b/Home.tsx index e1cc59c..432d027 100644 --- a/Home.tsx +++ b/Home.tsx @@ -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(); - const [id, setId] = useState(); const [offset, setOffset] = useState(0); - const [showEdit, setShowEdit] = useState(false); + const [edit, setEdit] = useState(); + 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() { ); @@ -82,13 +99,7 @@ export default function Home() { refreshing={refreshing} onRefresh={refreshLoader} /> - setId(undefined)} - id={id} - show={showEdit} - setShow={setShowEdit} - onSave={save} - /> + { - setId(undefined); - setShowEdit(true); + setEdit(undefined); + setShow(true); }} /> diff --git a/PlanItem.tsx b/PlanItem.tsx index 01a34d8..20a0339 100644 --- a/PlanItem.tsx +++ b/PlanItem.tsx @@ -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(); diff --git a/Plans.tsx b/Plans.tsx index c0aad30..9842a2c 100644 --- a/Plans.tsx +++ b/Plans.tsx @@ -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([]); 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}); diff --git a/SetItem.tsx b/SetItem.tsx index 5364803..bc4e331 100644 --- a/SetItem.tsx +++ b/SetItem.tsx @@ -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({ <> { - setId(item.id); + setSet(item); setShowEdit(true); }} title={item.name} diff --git a/Settings.tsx b/Settings.tsx index a1e48e3..ebfabec 100644 --- a/Settings.tsx +++ b/Settings.tsx @@ -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(''); @@ -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`); }; diff --git a/db.ts b/db.ts index 02673ab..7bfc740 100644 --- a/db.ts +++ b/db.ts @@ -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]));