diff --git a/DrawerMenu.tsx b/DrawerMenu.tsx new file mode 100644 index 0000000..c5c6d6b --- /dev/null +++ b/DrawerMenu.tsx @@ -0,0 +1,143 @@ +import {NavigationProp, useNavigation} from '@react-navigation/native'; +import React, {useCallback, useContext, useState} from 'react'; +import {ToastAndroid} from 'react-native'; +import DocumentPicker from 'react-native-document-picker'; +import {FileSystem} from 'react-native-file-access'; +import {Divider, IconButton, Menu} from 'react-native-paper'; +import {DatabaseContext, DrawerParamList} from './App'; +import ConfirmDialog from './ConfirmDialog'; +import {write} from './file'; +import {Plan} from './plan'; +import Set from './set'; + +const setFields = 'id,name,reps,weight,created,unit'; +const planFields = 'id,days,workouts'; + +export default function DrawerMenu({name}: {name: keyof DrawerParamList}) { + const [showMenu, setShowMenu] = useState(false); + const [showRemove, setShowRemove] = useState(false); + const db = useContext(DatabaseContext); + const {reset} = useNavigation>(); + + const exportSets = useCallback(async () => { + const [result] = await db.executeSql('SELECT * FROM sets'); + if (result.rows.length === 0) return; + const sets: Set[] = result.rows.raw(); + const data = [setFields] + .concat( + sets.map( + set => + `${set.id},${set.name},${set.reps},${set.weight},${set.created},${set.unit}`, + ), + ) + .join('\n'); + console.log(`${DrawerMenu.name}.exportSets`, {length: sets.length}); + await write('sets.csv', data); + }, [db]); + + const exportPlans = useCallback(async () => { + const [result] = await db.executeSql('SELECT * FROM plans'); + if (result.rows.length === 0) return; + const sets: Plan[] = result.rows.raw(); + const data = [planFields] + .concat(sets.map(set => `"${set.id}","${set.days}","${set.workouts}"`)) + .join('\n'); + console.log(`${DrawerMenu.name}.exportPlans`, {length: sets.length}); + await write('plans.csv', data); + }, [db]); + + const download = useCallback(async () => { + setShowMenu(false); + if (name === 'Home') exportSets(); + else if (name === 'Plans') exportPlans(); + }, [name, exportSets, exportPlans]); + + const uploadSets = useCallback(async () => { + const result = await DocumentPicker.pickSingle(); + const file = await FileSystem.readFile(result.uri); + console.log(`${DrawerMenu.name}.${uploadSets.name}:`, file.length); + const lines = file.split('\n'); + if (lines[0] != setFields) + return ToastAndroid.show('Invalid csv.', ToastAndroid.SHORT); + const values = lines + .slice(1) + .filter(line => line) + .map(set => { + const cells = set.split(','); + return `('${cells[1]}',${cells[2]},${cells[3]},'${cells[4]}','${cells[5]}')`; + }) + .join(','); + await db.executeSql( + `INSERT INTO sets(name,reps,weight,created,unit) VALUES ${values}`, + ); + ToastAndroid.show('Data imported.', ToastAndroid.SHORT); + reset({index: 0, routes: [{name}]}); + }, [db, reset, name]); + + const uploadPlans = useCallback(async () => { + const result = await DocumentPicker.pickSingle(); + const file = await FileSystem.readFile(result.uri); + console.log(`${DrawerMenu.name}.uploadPlans:`, file.length); + const lines = file.split('\n'); + if (lines[0] != planFields) + return ToastAndroid.show('Invalid csv.', ToastAndroid.SHORT); + const values = file + .split('\n') + .slice(1) + .filter(line => line) + .map(set => { + const cells = set.split('","').map(cell => cell.replace(/"/g, '')); + return `('${cells[1]}','${cells[2]}')`; + }) + .join(','); + await db.executeSql(`INSERT INTO plans(days,workouts) VALUES ${values}`); + ToastAndroid.show('Data imported.', ToastAndroid.SHORT); + }, [db]); + + const upload = useCallback(async () => { + setShowMenu(false); + if (name === 'Home') await uploadSets(); + else if (name === 'Plans') await uploadPlans(); + reset({index: 0, routes: [{name}]}); + }, [name, uploadPlans, uploadSets, reset]); + + const remove = useCallback(async () => { + setShowMenu(false); + setShowRemove(false); + if (name === 'Home') await db.executeSql(`DELETE FROM sets`); + else if (name === 'Plans') await db.executeSql(`DELETE FROM plans`); + ToastAndroid.show('All data has been deleted.', ToastAndroid.SHORT); + reset({index: 0, routes: [{name}]}); + }, [db, reset, name]); + + if (name === 'Home' || name === 'Plans') + return ( + setShowMenu(false)} + anchor={ + setShowMenu(true)} + icon="ellipsis-vertical" + /> + }> + + + + setShowRemove(true)} + title="Delete" + /> + + This irreversibly deletes all data from the app. Are you sure? + + + ); + + return null; +} diff --git a/HomePage.tsx b/HomePage.tsx index d1ed911..02c9fd6 100644 --- a/HomePage.tsx +++ b/HomePage.tsx @@ -25,6 +25,7 @@ export default function HomePage() { }} component={EditSet} listeners={{ beforeRemove: () => { diff --git a/Routes.tsx b/Routes.tsx index 058b0e4..3875183 100644 --- a/Routes.tsx +++ b/Routes.tsx @@ -4,6 +4,7 @@ import {IconButton} from 'react-native-paper'; import {SQLiteDatabase} from 'react-native-sqlite-storage'; import {DatabaseContext, Drawer, DrawerParamList} from './App'; import BestPage from './BestPage'; +import DrawerMenu from './DrawerMenu'; import HomePage from './HomePage'; import PlanPage from './PlanPage'; import SettingsPage from './SettingsPage'; @@ -36,6 +37,7 @@ export default function Routes({db}: {db: SQLiteDatabase | null}) { name={route.name} component={route.component} options={{ + headerRight: () => , drawerIcon: ({focused}) => ( (''); const [alarmEnabled, setAlarmEnabled] = useState(false); const [predictiveSets, setPredictiveSets] = useState(false); - const [snackbar, setSnackbar] = useState(''); const [showBattery, setShowBattery] = useState(false); - const [showDelete, setShowDelete] = useState(false); const [ignoring, setIgnoring] = useState(false); - const [timeoutId, setTimeoutId] = useState(0); - const db = useContext(DatabaseContext); const refresh = useCallback(async () => { setMinutes((await getItem('minutes')) || ''); @@ -43,70 +35,6 @@ export default function SettingsPage() { refresh(); }, [refresh]); - const toast = useCallback( - (message: string, timeout = 3000) => { - setSnackbar(message); - clearTimeout(timeoutId); - setTimeoutId(setTimeout(() => setSnackbar(''), timeout)); - }, - [setSnackbar, timeoutId, setTimeoutId], - ); - - const clear = useCallback(async () => { - setShowDelete(false); - await db.executeSql(`DELETE FROM sets`); - toast('All data has been deleted!'); - }, [db, toast]); - - const exportSets = useCallback(async () => { - const fileName = 'sets.csv'; - const filePath = `${Dirs.DocumentDir}/${fileName}`; - const [result] = await db.executeSql('SELECT * FROM sets'); - if (result.rows.length === 0) return; - const sets: Set[] = result.rows.raw(); - const data = ['id,name,reps,weight,created,unit'] - .concat( - sets.map( - set => - `${set.id},${set.name},${set.reps},${set.weight},${set.created},${set.unit}`, - ), - ) - .join('\n'); - console.log('SettingsPage.exportSets', {length: sets.length}); - const permission = async () => { - const granted = await PermissionsAndroid.request( - PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE, - ); - return granted === PermissionsAndroid.RESULTS.GRANTED; - }; - const granted = await permission(); - if (granted) { - await FileSystem.writeFile(filePath, data); - if (!FileSystem.exists(filePath)) return; - await FileSystem.cpExternal(filePath, fileName, 'downloads'); - } - toast('Exported data. Check your downloads folder.'); - }, [db, toast]); - - const importSets = useCallback(async () => { - const result = await DocumentPicker.pickSingle(); - const file = await FileSystem.readFile(result.uri); - console.log(`${SettingsPage.name}.${importSets.name}:`, file.length); - const values = file - .split('\n') - .slice(1) - .filter(line => line) - .map(set => { - const cells = set.split(','); - return `('${cells[1]}',${cells[2]},${cells[3]},'${cells[4]}','${cells[5]}')`; - }) - .join(','); - await db.executeSql( - `INSERT INTO sets(name,reps,weight,created,unit) VALUES ${values}`, - ); - toast('Data imported.'); - }, [db, toast]); - const changeAlarmEnabled = useCallback( (enabled: boolean) => { setAlarmEnabled(enabled); @@ -120,9 +48,12 @@ export default function SettingsPage() { (enabled: boolean) => { setPredictiveSets(enabled); setItem('predictiveSets', enabled ? 'true' : 'false'); - toast('Predictive sets guess whats next based on todays plan.', 10000); + ToastAndroid.show( + 'Predictive sets guess whats next based on todays plan.', + ToastAndroid.LONG, + ); }, - [setPredictiveSets, toast], + [setPredictiveSets], ); return ( @@ -186,42 +117,6 @@ export default function SettingsPage() { value={predictiveSets} onValueChange={changePredictive} /> - - - - - - - - - This irreversibly deletes all data from the app. Are you sure? - - - setSnackbar('')} - action={{label: 'Close', onPress: () => setSnackbar('')}}> - {snackbar} - ); } diff --git a/file.ts b/file.ts new file mode 100644 index 0000000..3f288ab --- /dev/null +++ b/file.ts @@ -0,0 +1,18 @@ +import {PermissionsAndroid, ToastAndroid} from 'react-native'; +import {Dirs, FileSystem} from 'react-native-file-access'; + +export const write = async (name: string, data: string) => { + const filePath = `${Dirs.DocumentDir}/${name}`; + const permission = async () => { + const granted = await PermissionsAndroid.request( + PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE, + ); + return granted === PermissionsAndroid.RESULTS.GRANTED; + }; + const granted = await permission(); + if (!granted) return; + await FileSystem.writeFile(filePath, data); + if (!FileSystem.exists(filePath)) return; + await FileSystem.cpExternal(filePath, name, 'downloads'); + ToastAndroid.show(`Saved "${name}". Check downloads`, ToastAndroid.LONG); +};