Move uploading/downloading/deleting into the top bar

This commit is contained in:
Brandon Presley 2022-07-19 16:24:16 +12:00
parent 42c2502e12
commit c0b6ba8606
5 changed files with 172 additions and 113 deletions

143
DrawerMenu.tsx Normal file
View File

@ -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<NavigationProp<DrawerParamList>>();
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 (
<Menu
visible={showMenu}
onDismiss={() => setShowMenu(false)}
anchor={
<IconButton
onPress={() => setShowMenu(true)}
icon="ellipsis-vertical"
/>
}>
<Menu.Item icon="arrow-down" onPress={download} title="Download" />
<Menu.Item icon="arrow-up" onPress={upload} title="Upload" />
<Divider />
<Menu.Item
icon="trash"
onPress={() => setShowRemove(true)}
title="Delete"
/>
<ConfirmDialog
title="Delete all data"
show={showRemove}
setShow={setShowRemove}
onOk={remove}>
This irreversibly deletes all data from the app. Are you sure?
</ConfirmDialog>
</Menu>
);
return null;
}

View File

@ -25,6 +25,7 @@ export default function HomePage() {
<Stack.Screen name="Sets" component={SetList} />
<Stack.Screen
name="EditSet"
options={{headerRight: () => <IconButton icon="clock" />}}
component={EditSet}
listeners={{
beforeRemove: () => {

View File

@ -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: () => <DrawerMenu name={route.name} />,
drawerIcon: ({focused}) => (
<IconButton
icon={focused ? route.icon : `${route.icon}-outline`}

View File

@ -1,19 +1,15 @@
import AsyncStorage from '@react-native-async-storage/async-storage';
import React, {useCallback, useContext, useEffect, useState} from 'react';
import React, {useCallback, useEffect, useState} from 'react';
import {
NativeModules,
PermissionsAndroid,
StyleSheet,
Text,
ToastAndroid,
View,
} from 'react-native';
import DocumentPicker from 'react-native-document-picker';
import {Dirs, FileSystem} from 'react-native-file-access';
import {Button, Snackbar, TextInput} from 'react-native-paper';
import {DatabaseContext} from './App';
import {TextInput} from 'react-native-paper';
import ConfirmDialog from './ConfirmDialog';
import MassiveSwitch from './MassiveSwitch';
import Set from './set';
const {getItem, setItem} = AsyncStorage;
@ -23,12 +19,8 @@ export default function SettingsPage() {
const [seconds, setSeconds] = useState<string>('');
const [alarmEnabled, setAlarmEnabled] = useState<boolean>(false);
const [predictiveSets, setPredictiveSets] = useState<boolean>(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}
/>
<Button
style={{alignSelf: 'flex-start'}}
icon="arrow-down"
onPress={exportSets}>
Export
</Button>
<Button
style={{alignSelf: 'flex-start'}}
icon="arrow-up"
onPress={importSets}>
Import
</Button>
<Button
style={{alignSelf: 'flex-start'}}
icon="trash"
onPress={() => setShowDelete(true)}>
Delete all data
</Button>
<ConfirmDialog
title="Delete all data"
show={showDelete}
setShow={setShowDelete}
onOk={clear}>
This irreversibly deletes all data from the app. Are you sure?
</ConfirmDialog>
<Snackbar
visible={!!snackbar}
onDismiss={() => setSnackbar('')}
action={{label: 'Close', onPress: () => setSnackbar('')}}>
{snackbar}
</Snackbar>
</View>
);
}

18
file.ts Normal file
View File

@ -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);
};