Move uploading/downloading/deleting into the top bar
This commit is contained in:
parent
42c2502e12
commit
c0b6ba8606
143
DrawerMenu.tsx
Normal file
143
DrawerMenu.tsx
Normal 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;
|
||||||
|
}
|
|
@ -25,6 +25,7 @@ export default function HomePage() {
|
||||||
<Stack.Screen name="Sets" component={SetList} />
|
<Stack.Screen name="Sets" component={SetList} />
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="EditSet"
|
name="EditSet"
|
||||||
|
options={{headerRight: () => <IconButton icon="clock" />}}
|
||||||
component={EditSet}
|
component={EditSet}
|
||||||
listeners={{
|
listeners={{
|
||||||
beforeRemove: () => {
|
beforeRemove: () => {
|
||||||
|
|
|
@ -4,6 +4,7 @@ import {IconButton} from 'react-native-paper';
|
||||||
import {SQLiteDatabase} from 'react-native-sqlite-storage';
|
import {SQLiteDatabase} from 'react-native-sqlite-storage';
|
||||||
import {DatabaseContext, Drawer, DrawerParamList} from './App';
|
import {DatabaseContext, Drawer, DrawerParamList} from './App';
|
||||||
import BestPage from './BestPage';
|
import BestPage from './BestPage';
|
||||||
|
import DrawerMenu from './DrawerMenu';
|
||||||
import HomePage from './HomePage';
|
import HomePage from './HomePage';
|
||||||
import PlanPage from './PlanPage';
|
import PlanPage from './PlanPage';
|
||||||
import SettingsPage from './SettingsPage';
|
import SettingsPage from './SettingsPage';
|
||||||
|
@ -36,6 +37,7 @@ export default function Routes({db}: {db: SQLiteDatabase | null}) {
|
||||||
name={route.name}
|
name={route.name}
|
||||||
component={route.component}
|
component={route.component}
|
||||||
options={{
|
options={{
|
||||||
|
headerRight: () => <DrawerMenu name={route.name} />,
|
||||||
drawerIcon: ({focused}) => (
|
drawerIcon: ({focused}) => (
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={focused ? route.icon : `${route.icon}-outline`}
|
icon={focused ? route.icon : `${route.icon}-outline`}
|
||||||
|
|
121
SettingsPage.tsx
121
SettingsPage.tsx
|
@ -1,19 +1,15 @@
|
||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
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 {
|
import {
|
||||||
NativeModules,
|
NativeModules,
|
||||||
PermissionsAndroid,
|
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
Text,
|
Text,
|
||||||
|
ToastAndroid,
|
||||||
View,
|
View,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import DocumentPicker from 'react-native-document-picker';
|
import {TextInput} from 'react-native-paper';
|
||||||
import {Dirs, FileSystem} from 'react-native-file-access';
|
|
||||||
import {Button, Snackbar, TextInput} from 'react-native-paper';
|
|
||||||
import {DatabaseContext} from './App';
|
|
||||||
import ConfirmDialog from './ConfirmDialog';
|
import ConfirmDialog from './ConfirmDialog';
|
||||||
import MassiveSwitch from './MassiveSwitch';
|
import MassiveSwitch from './MassiveSwitch';
|
||||||
import Set from './set';
|
|
||||||
|
|
||||||
const {getItem, setItem} = AsyncStorage;
|
const {getItem, setItem} = AsyncStorage;
|
||||||
|
|
||||||
|
@ -23,12 +19,8 @@ export default function SettingsPage() {
|
||||||
const [seconds, setSeconds] = useState<string>('');
|
const [seconds, setSeconds] = useState<string>('');
|
||||||
const [alarmEnabled, setAlarmEnabled] = useState<boolean>(false);
|
const [alarmEnabled, setAlarmEnabled] = useState<boolean>(false);
|
||||||
const [predictiveSets, setPredictiveSets] = useState<boolean>(false);
|
const [predictiveSets, setPredictiveSets] = useState<boolean>(false);
|
||||||
const [snackbar, setSnackbar] = useState('');
|
|
||||||
const [showBattery, setShowBattery] = useState(false);
|
const [showBattery, setShowBattery] = useState(false);
|
||||||
const [showDelete, setShowDelete] = useState(false);
|
|
||||||
const [ignoring, setIgnoring] = useState(false);
|
const [ignoring, setIgnoring] = useState(false);
|
||||||
const [timeoutId, setTimeoutId] = useState(0);
|
|
||||||
const db = useContext(DatabaseContext);
|
|
||||||
|
|
||||||
const refresh = useCallback(async () => {
|
const refresh = useCallback(async () => {
|
||||||
setMinutes((await getItem('minutes')) || '');
|
setMinutes((await getItem('minutes')) || '');
|
||||||
|
@ -43,70 +35,6 @@ export default function SettingsPage() {
|
||||||
refresh();
|
refresh();
|
||||||
}, [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(
|
const changeAlarmEnabled = useCallback(
|
||||||
(enabled: boolean) => {
|
(enabled: boolean) => {
|
||||||
setAlarmEnabled(enabled);
|
setAlarmEnabled(enabled);
|
||||||
|
@ -120,9 +48,12 @@ export default function SettingsPage() {
|
||||||
(enabled: boolean) => {
|
(enabled: boolean) => {
|
||||||
setPredictiveSets(enabled);
|
setPredictiveSets(enabled);
|
||||||
setItem('predictiveSets', enabled ? 'true' : 'false');
|
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 (
|
return (
|
||||||
|
@ -186,42 +117,6 @@ export default function SettingsPage() {
|
||||||
value={predictiveSets}
|
value={predictiveSets}
|
||||||
onValueChange={changePredictive}
|
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>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
18
file.ts
Normal file
18
file.ts
Normal 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);
|
||||||
|
};
|
Loading…
Reference in New Issue
Block a user