Remove semicolons from line endings

This commit is contained in:
Brandon Presley 2022-10-31 17:22:08 +13:00
parent 1bc145f60c
commit bc7aca03e8
54 changed files with 1119 additions and 1122 deletions

View File

@ -4,4 +4,5 @@ module.exports = {
bracketSpacing: false, bracketSpacing: false,
singleQuote: true, singleQuote: true,
trailingComma: 'all', trailingComma: 'all',
} semi: false,
};

66
App.tsx
View File

@ -2,22 +2,22 @@ import {
DarkTheme as NavigationDarkTheme, DarkTheme as NavigationDarkTheme,
DefaultTheme as NavigationDefaultTheme, DefaultTheme as NavigationDefaultTheme,
NavigationContainer, NavigationContainer,
} from '@react-navigation/native'; } from '@react-navigation/native'
import {useEffect, useMemo, useState} from 'react'; import {useEffect, useMemo, useState} from 'react'
import {useColorScheme} from 'react-native'; import {useColorScheme} from 'react-native'
import { import {
DarkTheme as PaperDarkTheme, DarkTheme as PaperDarkTheme,
DefaultTheme as PaperDefaultTheme, DefaultTheme as PaperDefaultTheme,
Provider as PaperProvider, Provider as PaperProvider,
} from 'react-native-paper'; } from 'react-native-paper'
import MaterialIcon from 'react-native-vector-icons/MaterialIcons'; import MaterialIcon from 'react-native-vector-icons/MaterialIcons'
import {Color} from './color'; import {Color} from './color'
import {lightColors} from './colors'; import {lightColors} from './colors'
import {runMigrations, settingsRepo} from './db'; import {runMigrations, settingsRepo} from './db'
import MassiveSnack from './MassiveSnack'; import MassiveSnack from './MassiveSnack'
import Routes from './Routes'; import Routes from './Routes'
import Settings from './settings'; import Settings from './settings'
import {SettingsContext} from './use-settings'; import {SettingsContext} from './use-settings'
export const CombinedDefaultTheme = { export const CombinedDefaultTheme = {
...NavigationDefaultTheme, ...NavigationDefaultTheme,
@ -26,7 +26,7 @@ export const CombinedDefaultTheme = {
...NavigationDefaultTheme.colors, ...NavigationDefaultTheme.colors,
...PaperDefaultTheme.colors, ...PaperDefaultTheme.colors,
}, },
}; }
export const CombinedDarkTheme = { export const CombinedDarkTheme = {
...NavigationDarkTheme, ...NavigationDarkTheme,
@ -37,40 +37,40 @@ export const CombinedDarkTheme = {
primary: lightColors[0].hex, primary: lightColors[0].hex,
background: '#0E0E0E', background: '#0E0E0E',
}, },
}; }
const App = () => { const App = () => {
const isDark = useColorScheme() === 'dark'; const isDark = useColorScheme() === 'dark'
const [settings, setSettings] = useState<Settings>(); const [settings, setSettings] = useState<Settings>()
const [color, setColor] = useState( const [color, setColor] = useState(
isDark isDark
? CombinedDarkTheme.colors.primary.toUpperCase() ? CombinedDarkTheme.colors.primary.toUpperCase()
: CombinedDefaultTheme.colors.primary.toUpperCase(), : CombinedDefaultTheme.colors.primary.toUpperCase(),
); )
useEffect(() => { useEffect(() => {
runMigrations().then(async () => { runMigrations().then(async () => {
const gotSettings = await settingsRepo.findOne({where: {}}); const gotSettings = await settingsRepo.findOne({where: {}})
console.log(`${App.name}.runMigrations:`, {gotSettings}); console.log(`${App.name}.runMigrations:`, {gotSettings})
setSettings(gotSettings); setSettings(gotSettings)
if (gotSettings.color) setColor(gotSettings.color); if (gotSettings.color) setColor(gotSettings.color)
}); })
}, [setColor]); }, [setColor])
const theme = useMemo(() => { const theme = useMemo(() => {
const darkTheme = { const darkTheme = {
...CombinedDarkTheme, ...CombinedDarkTheme,
colors: {...CombinedDarkTheme.colors, primary: color}, colors: {...CombinedDarkTheme.colors, primary: color},
}; }
const lightTheme = { const lightTheme = {
...CombinedDefaultTheme, ...CombinedDefaultTheme,
colors: {...CombinedDefaultTheme.colors, primary: color}, colors: {...CombinedDefaultTheme.colors, primary: color},
}; }
let value = isDark ? darkTheme : lightTheme; let value = isDark ? darkTheme : lightTheme
if (settings?.theme === 'dark') value = darkTheme; if (settings?.theme === 'dark') value = darkTheme
else if (settings?.theme === 'light') value = lightTheme; else if (settings?.theme === 'light') value = lightTheme
return value; return value
}, [color, isDark, settings]); }, [color, isDark, settings])
return ( return (
<Color.Provider value={{color, setColor}}> <Color.Provider value={{color, setColor}}>
@ -88,7 +88,7 @@ const App = () => {
</NavigationContainer> </NavigationContainer>
</PaperProvider> </PaperProvider>
</Color.Provider> </Color.Provider>
); )
}; }
export default App; export default App

View File

@ -2,22 +2,22 @@ import {
NavigationProp, NavigationProp,
useFocusEffect, useFocusEffect,
useNavigation, useNavigation,
} from '@react-navigation/native'; } from '@react-navigation/native'
import {useCallback, useState} from 'react'; import {useCallback, useState} from 'react'
import {FlatList, Image} from 'react-native'; import {FlatList, Image} from 'react-native'
import {List} from 'react-native-paper'; import {List} from 'react-native-paper'
import {BestPageParams} from './BestPage'; import {BestPageParams} from './BestPage'
import {setRepo} from './db'; import {setRepo} from './db'
import DrawerHeader from './DrawerHeader'; import DrawerHeader from './DrawerHeader'
import GymSet from './gym-set'; import GymSet from './gym-set'
import Page from './Page'; import Page from './Page'
import {useSettings} from './use-settings'; import {useSettings} from './use-settings'
export default function BestList() { export default function BestList() {
const [bests, setBests] = useState<GymSet[]>(); const [bests, setBests] = useState<GymSet[]>()
const [term, setTerm] = useState(''); const [term, setTerm] = useState('')
const navigation = useNavigation<NavigationProp<BestPageParams>>(); const navigation = useNavigation<NavigationProp<BestPageParams>>()
const {settings} = useSettings(); const {settings} = useSettings()
const refresh = useCallback(async (value: string) => { const refresh = useCallback(async (value: string) => {
const weights = await setRepo const weights = await setRepo
@ -27,9 +27,9 @@ export default function BestList() {
.where('name LIKE :name', {name: `%${value}%`}) .where('name LIKE :name', {name: `%${value}%`})
.andWhere('NOT hidden') .andWhere('NOT hidden')
.groupBy('name') .groupBy('name')
.getMany(); .getMany()
console.log(`${BestList.name}.refresh:`, {length: weights.length}); console.log(`${BestList.name}.refresh:`, {length: weights.length})
let newBest: GymSet[] = []; let newBest: GymSet[] = []
for (const set of weights) { for (const set of weights) {
const reps = await setRepo const reps = await setRepo
.createQueryBuilder() .createQueryBuilder()
@ -39,25 +39,25 @@ export default function BestList() {
.andWhere('weight = :weight', {weight: set.weight}) .andWhere('weight = :weight', {weight: set.weight})
.andWhere('NOT hidden') .andWhere('NOT hidden')
.groupBy('name') .groupBy('name')
.getMany(); .getMany()
newBest.push(...reps); newBest.push(...reps)
} }
setBests(newBest); setBests(newBest)
}, []); }, [])
useFocusEffect( useFocusEffect(
useCallback(() => { useCallback(() => {
refresh(term); refresh(term)
}, [refresh, term]), }, [refresh, term]),
); )
const search = useCallback( const search = useCallback(
(value: string) => { (value: string) => {
setTerm(value); setTerm(value)
refresh(value); refresh(value)
}, },
[refresh], [refresh],
); )
const renderItem = ({item}: {item: GymSet}) => ( const renderItem = ({item}: {item: GymSet}) => (
<List.Item <List.Item
@ -72,7 +72,7 @@ export default function BestList() {
null null
} }
/> />
); )
return ( return (
<> <>
@ -88,5 +88,5 @@ export default function BestList() {
)} )}
</Page> </Page>
</> </>
); )
} }

View File

@ -1,15 +1,15 @@
import {createStackNavigator} from '@react-navigation/stack'; import {createStackNavigator} from '@react-navigation/stack'
import BestList from './BestList'; import BestList from './BestList'
import GymSet from './gym-set'; import GymSet from './gym-set'
import ViewBest from './ViewBest'; import ViewBest from './ViewBest'
const Stack = createStackNavigator<BestPageParams>(); const Stack = createStackNavigator<BestPageParams>()
export type BestPageParams = { export type BestPageParams = {
BestList: {}; BestList: {}
ViewBest: { ViewBest: {
best: GymSet; best: GymSet
}; }
}; }
export default function BestPage() { export default function BestPage() {
return ( return (
@ -18,5 +18,5 @@ export default function BestPage() {
<Stack.Screen name="BestList" component={BestList} /> <Stack.Screen name="BestList" component={BestList} />
<Stack.Screen name="ViewBest" component={ViewBest} /> <Stack.Screen name="ViewBest" component={ViewBest} />
</Stack.Navigator> </Stack.Navigator>
); )
} }

View File

@ -1,11 +1,11 @@
import * as shape from 'd3-shape'; import * as shape from 'd3-shape'
import {View} from 'react-native'; import {View} from 'react-native'
import {Grid, LineChart, XAxis, YAxis} from 'react-native-svg-charts'; import {Grid, LineChart, XAxis, YAxis} from 'react-native-svg-charts'
import {CombinedDarkTheme, CombinedDefaultTheme} from './App'; import {CombinedDarkTheme, CombinedDefaultTheme} from './App'
import {useColor} from './color'; import {useColor} from './color'
import {MARGIN, PADDING} from './constants'; import {MARGIN, PADDING} from './constants'
import GymSet from './gym-set'; import GymSet from './gym-set'
import useDark from './use-dark'; import useDark from './use-dark'
export default function Chart({ export default function Chart({
yData, yData,
@ -13,21 +13,21 @@ export default function Chart({
xData, xData,
yFormat, yFormat,
}: { }: {
yData: number[]; yData: number[]
xData: GymSet[]; xData: GymSet[]
xFormat: (value: any, index: number) => string; xFormat: (value: any, index: number) => string
yFormat: (value: any) => string; yFormat: (value: any) => string
}) { }) {
const {color} = useColor(); const {color} = useColor()
const dark = useDark(); const dark = useDark()
const axesSvg = { const axesSvg = {
fontSize: 10, fontSize: 10,
fill: dark fill: dark
? CombinedDarkTheme.colors.text ? CombinedDarkTheme.colors.text
: CombinedDefaultTheme.colors.text, : CombinedDefaultTheme.colors.text,
}; }
const verticalContentInset = {top: 10, bottom: 10}; const verticalContentInset = {top: 10, bottom: 10}
const xAxisHeight = 30; const xAxisHeight = 30
return ( return (
<> <>
@ -60,5 +60,5 @@ export default function Chart({
</View> </View>
</View> </View>
</> </>
); )
} }

View File

@ -1,4 +1,4 @@
import {Button, Dialog, Portal, Text} from 'react-native-paper'; import {Button, Dialog, Portal, Text} from 'react-native-paper'
export default function ConfirmDialog({ export default function ConfirmDialog({
title, title,
@ -7,11 +7,11 @@ export default function ConfirmDialog({
show, show,
setShow, setShow,
}: { }: {
title: string; title: string
children: JSX.Element | JSX.Element[] | string; children: JSX.Element | JSX.Element[] | string
onOk: () => void; onOk: () => void
show: boolean; show: boolean
setShow: (show: boolean) => void; setShow: (show: boolean) => void
}) { }) {
return ( return (
<Portal> <Portal>
@ -26,5 +26,5 @@ export default function ConfirmDialog({
</Dialog.Actions> </Dialog.Actions>
</Dialog> </Dialog>
</Portal> </Portal>
); )
} }

View File

@ -1,13 +1,13 @@
import {DrawerNavigationProp} from '@react-navigation/drawer'; import {DrawerNavigationProp} from '@react-navigation/drawer'
import {useNavigation} from '@react-navigation/native'; import {useNavigation} from '@react-navigation/native'
import {Appbar, IconButton} from 'react-native-paper'; import {Appbar, IconButton} from 'react-native-paper'
import {DrawerParamList} from './drawer-param-list'; import {DrawerParamList} from './drawer-param-list'
import DrawerMenu from './DrawerMenu'; import DrawerMenu from './DrawerMenu'
import useDark from './use-dark'; import useDark from './use-dark'
export default function DrawerHeader({name}: {name: keyof DrawerParamList}) { export default function DrawerHeader({name}: {name: keyof DrawerParamList}) {
const navigation = useNavigation<DrawerNavigationProp<DrawerParamList>>(); const navigation = useNavigation<DrawerNavigationProp<DrawerParamList>>()
const dark = useDark(); const dark = useDark()
return ( return (
<Appbar.Header> <Appbar.Header>
@ -19,5 +19,5 @@ export default function DrawerHeader({name}: {name: keyof DrawerParamList}) {
<Appbar.Content title={name} /> <Appbar.Content title={name} />
<DrawerMenu name={name} /> <DrawerMenu name={name} />
</Appbar.Header> </Appbar.Header>
); )
} }

View File

@ -1,72 +1,71 @@
import {NavigationProp, useNavigation} from '@react-navigation/native'; import {NavigationProp, useNavigation} from '@react-navigation/native'
import {useCallback, useState} from 'react'; import {useCallback, useState} from 'react'
import DocumentPicker from 'react-native-document-picker'; import DocumentPicker from 'react-native-document-picker'
import {FileSystem} from 'react-native-file-access'; import {FileSystem} from 'react-native-file-access'
import {Divider, IconButton, Menu} from 'react-native-paper'; import {Divider, IconButton, Menu} from 'react-native-paper'
import ConfirmDialog from './ConfirmDialog'; import ConfirmDialog from './ConfirmDialog'
import {AppDataSource} from './data-source'; import {AppDataSource} from './data-source'
import {planRepo} from './db'; import {planRepo} from './db'
import {DrawerParamList} from './drawer-param-list'; import {DrawerParamList} from './drawer-param-list'
import GymSet from './gym-set'; import GymSet from './gym-set'
import {useSnackbar} from './MassiveSnack'; import {useSnackbar} from './MassiveSnack'
import {Plan} from './plan'; import {Plan} from './plan'
import useDark from './use-dark'; import useDark from './use-dark'
import {write} from './write'; import {write} from './write'
const setFields = const setFields = 'id,name,reps,weight,created,unit,hidden,sets,minutes,seconds'
'id,name,reps,weight,created,unit,hidden,sets,minutes,seconds'; const planFields = 'id,days,workouts'
const planFields = 'id,days,workouts'; const setRepo = AppDataSource.manager.getRepository(GymSet)
const setRepo = AppDataSource.manager.getRepository(GymSet);
export default function DrawerMenu({name}: {name: keyof DrawerParamList}) { export default function DrawerMenu({name}: {name: keyof DrawerParamList}) {
const [showMenu, setShowMenu] = useState(false); const [showMenu, setShowMenu] = useState(false)
const [showRemove, setShowRemove] = useState(false); const [showRemove, setShowRemove] = useState(false)
const {toast} = useSnackbar(); const {toast} = useSnackbar()
const {reset} = useNavigation<NavigationProp<DrawerParamList>>(); const {reset} = useNavigation<NavigationProp<DrawerParamList>>()
const dark = useDark(); const dark = useDark()
const exportSets = useCallback(async () => { const exportSets = useCallback(async () => {
const sets = await setRepo.find({}); const sets = await setRepo.find({})
const data = [setFields] const data = [setFields]
.concat( .concat(
sets.map(set => sets.map(set =>
setFields setFields
.split(',') .split(',')
.map(fieldString => { .map(fieldString => {
const field = fieldString as keyof GymSet; const field = fieldString as keyof GymSet
if (field === 'unit') return set[field] || 'kg'; if (field === 'unit') return set[field] || 'kg'
return set[field]; return set[field]
}) })
.join(','), .join(','),
), ),
) )
.join('\n'); .join('\n')
console.log(`${DrawerMenu.name}.exportSets`, {length: sets.length}); console.log(`${DrawerMenu.name}.exportSets`, {length: sets.length})
await write('sets.csv', data); await write('sets.csv', data)
}, []); }, [])
const exportPlans = useCallback(async () => { const exportPlans = useCallback(async () => {
const plans = await planRepo.find({}); const plans = await planRepo.find({})
const data = [planFields] const data = [planFields]
.concat(plans.map(set => `"${set.id}","${set.days}","${set.workouts}"`)) .concat(plans.map(set => `"${set.id}","${set.days}","${set.workouts}"`))
.join('\n'); .join('\n')
console.log(`${DrawerMenu.name}.exportPlans`, {length: plans.length}); console.log(`${DrawerMenu.name}.exportPlans`, {length: plans.length})
await write('plans.csv', data); await write('plans.csv', data)
}, []); }, [])
const download = useCallback(async () => { const download = useCallback(async () => {
setShowMenu(false); setShowMenu(false)
if (name === 'Home') exportSets(); if (name === 'Home') exportSets()
else if (name === 'Plans') exportPlans(); else if (name === 'Plans') exportPlans()
}, [name, exportSets, exportPlans]); }, [name, exportSets, exportPlans])
const uploadSets = useCallback(async () => { const uploadSets = useCallback(async () => {
const result = await DocumentPicker.pickSingle(); const result = await DocumentPicker.pickSingle()
const file = await FileSystem.readFile(result.uri); const file = await FileSystem.readFile(result.uri)
console.log(`${DrawerMenu.name}.uploadSets:`, file.length); console.log(`${DrawerMenu.name}.uploadSets:`, file.length)
const lines = file.split('\n'); const lines = file.split('\n')
console.log(lines[0]); console.log(lines[0])
if (!setFields.includes(lines[0])) return toast('Invalid csv.', 3000); if (!setFields.includes(lines[0])) return toast('Invalid csv.', 3000)
const values = lines const values = lines
.slice(1) .slice(1)
.filter(line => line) .filter(line => line)
@ -82,7 +81,7 @@ export default function DrawerMenu({name}: {name: keyof DrawerParamList}) {
sets, sets,
minutes, minutes,
seconds, seconds,
] = line.split(','); ] = line.split(',')
const set: GymSet = { const set: GymSet = {
name: setName, name: setName,
reps: +reps, reps: +reps,
@ -93,21 +92,21 @@ export default function DrawerMenu({name}: {name: keyof DrawerParamList}) {
sets: +sets, sets: +sets,
minutes: +minutes, minutes: +minutes,
seconds: +seconds, seconds: +seconds,
}; }
return set; return set
}); })
console.log(`${DrawerMenu.name}.uploadSets:`, {values}); console.log(`${DrawerMenu.name}.uploadSets:`, {values})
await setRepo.insert(values); await setRepo.insert(values)
toast('Data imported.', 3000); toast('Data imported.', 3000)
reset({index: 0, routes: [{name}]}); reset({index: 0, routes: [{name}]})
}, [reset, name, toast]); }, [reset, name, toast])
const uploadPlans = useCallback(async () => { const uploadPlans = useCallback(async () => {
const result = await DocumentPicker.pickSingle(); const result = await DocumentPicker.pickSingle()
const file = await FileSystem.readFile(result.uri); const file = await FileSystem.readFile(result.uri)
console.log(`${DrawerMenu.name}.uploadPlans:`, file.length); console.log(`${DrawerMenu.name}.uploadPlans:`, file.length)
const lines = file.split('\n'); const lines = file.split('\n')
if (lines[0] != planFields) return toast('Invalid csv.', 3000); if (lines[0] != planFields) return toast('Invalid csv.', 3000)
const values = file const values = file
.split('\n') .split('\n')
.slice(1) .slice(1)
@ -115,32 +114,32 @@ export default function DrawerMenu({name}: {name: keyof DrawerParamList}) {
.map(set => { .map(set => {
const [, days, workouts] = set const [, days, workouts] = set
.split('","') .split('","')
.map(cell => cell.replace(/"/g, '')); .map(cell => cell.replace(/"/g, ''))
const plan: Plan = { const plan: Plan = {
days, days,
workouts, workouts,
}; }
return plan; return plan
}); })
await planRepo.insert(values); await planRepo.insert(values)
toast('Data imported.', 3000); toast('Data imported.', 3000)
}, [toast]); }, [toast])
const upload = useCallback(async () => { const upload = useCallback(async () => {
setShowMenu(false); setShowMenu(false)
if (name === 'Home') await uploadSets(); if (name === 'Home') await uploadSets()
else if (name === 'Plans') await uploadPlans(); else if (name === 'Plans') await uploadPlans()
reset({index: 0, routes: [{name}]}); reset({index: 0, routes: [{name}]})
}, [name, uploadPlans, uploadSets, reset]); }, [name, uploadPlans, uploadSets, reset])
const remove = useCallback(async () => { const remove = useCallback(async () => {
setShowMenu(false); setShowMenu(false)
setShowRemove(false); setShowRemove(false)
if (name === 'Home') await setRepo.delete({}); if (name === 'Home') await setRepo.delete({})
else if (name === 'Plans') await planRepo.delete({}); else if (name === 'Plans') await planRepo.delete({})
toast('All data has been deleted.', 4000); toast('All data has been deleted.', 4000)
reset({index: 0, routes: [{name}]}); reset({index: 0, routes: [{name}]})
}, [reset, name, toast]); }, [reset, name, toast])
if (name === 'Home' || name === 'Plans') if (name === 'Home' || name === 'Plans')
return ( return (
@ -170,7 +169,7 @@ export default function DrawerMenu({name}: {name: keyof DrawerParamList}) {
This irreversibly deletes all data from the app. Are you sure? This irreversibly deletes all data from the app. Are you sure?
</ConfirmDialog> </ConfirmDialog>
</Menu> </Menu>
); )
return null; return null
} }

View File

@ -3,29 +3,29 @@ import {
RouteProp, RouteProp,
useNavigation, useNavigation,
useRoute, useRoute,
} from '@react-navigation/native'; } from '@react-navigation/native'
import {useCallback, useEffect, useState} from 'react'; import {useCallback, useEffect, useState} from 'react'
import {ScrollView, StyleSheet, View} from 'react-native'; import {ScrollView, StyleSheet, View} from 'react-native'
import {Button, Text} from 'react-native-paper'; import {Button, Text} from 'react-native-paper'
import {MARGIN, PADDING} from './constants'; import {MARGIN, PADDING} from './constants'
import {planRepo, setRepo} from './db'; import {planRepo, setRepo} from './db'
import {DrawerParamList} from './drawer-param-list'; import {DrawerParamList} from './drawer-param-list'
import {PlanPageParams} from './plan-page-params'; import {PlanPageParams} from './plan-page-params'
import StackHeader from './StackHeader'; import StackHeader from './StackHeader'
import Switch from './Switch'; import Switch from './Switch'
import {DAYS} from './time'; import {DAYS} from './time'
export default function EditPlan() { export default function EditPlan() {
const {params} = useRoute<RouteProp<PlanPageParams, 'EditPlan'>>(); const {params} = useRoute<RouteProp<PlanPageParams, 'EditPlan'>>()
const {plan} = params; const {plan} = params
const [days, setDays] = useState<string[]>( const [days, setDays] = useState<string[]>(
plan.days ? plan.days.split(',') : [], plan.days ? plan.days.split(',') : [],
); )
const [workouts, setWorkouts] = useState<string[]>( const [workouts, setWorkouts] = useState<string[]>(
plan.workouts ? plan.workouts.split(',') : [], plan.workouts ? plan.workouts.split(',') : [],
); )
const [names, setNames] = useState<string[]>([]); const [names, setNames] = useState<string[]>([])
const navigation = useNavigation<NavigationProp<DrawerParamList>>(); const navigation = useNavigation<NavigationProp<DrawerParamList>>()
useEffect(() => { useEffect(() => {
setRepo setRepo
@ -34,41 +34,41 @@ export default function EditPlan() {
.distinct(true) .distinct(true)
.getRawMany() .getRawMany()
.then(values => { .then(values => {
console.log(EditPlan.name, {values}); console.log(EditPlan.name, {values})
setNames(values.map(value => value.name)); setNames(values.map(value => value.name))
}); })
}, []); }, [])
const save = useCallback(async () => { const save = useCallback(async () => {
console.log(`${EditPlan.name}.save`, {days, workouts, plan}); console.log(`${EditPlan.name}.save`, {days, workouts, plan})
if (!days || !workouts) return; if (!days || !workouts) return
const newWorkouts = workouts.filter(workout => workout).join(','); const newWorkouts = workouts.filter(workout => workout).join(',')
const newDays = days.filter(day => day).join(','); const newDays = days.filter(day => day).join(',')
await planRepo.save({days: newDays, workouts: newWorkouts, id: plan.id}); await planRepo.save({days: newDays, workouts: newWorkouts, id: plan.id})
navigation.goBack(); navigation.goBack()
}, [days, workouts, plan, navigation]); }, [days, workouts, plan, navigation])
const toggleWorkout = useCallback( const toggleWorkout = useCallback(
(on: boolean, name: string) => { (on: boolean, name: string) => {
if (on) { if (on) {
setWorkouts([...workouts, name]); setWorkouts([...workouts, name])
} else { } else {
setWorkouts(workouts.filter(workout => workout !== name)); setWorkouts(workouts.filter(workout => workout !== name))
} }
}, },
[setWorkouts, workouts], [setWorkouts, workouts],
); )
const toggleDay = useCallback( const toggleDay = useCallback(
(on: boolean, day: string) => { (on: boolean, day: string) => {
if (on) { if (on) {
setDays([...days, day]); setDays([...days, day])
} else { } else {
setDays(days.filter(d => d !== day)); setDays(days.filter(d => d !== day))
} }
}, },
[setDays, days], [setDays, days],
); )
return ( return (
<> <>
@ -107,11 +107,11 @@ export default function EditPlan() {
disabled={workouts.length === 0 && days.length === 0} disabled={workouts.length === 0 && days.length === 0}
mode="contained" mode="contained"
onPress={() => { onPress={() => {
navigation.goBack(); navigation.goBack()
navigation.navigate('Workouts', { navigation.navigate('Workouts', {
screen: 'EditWorkout', screen: 'EditWorkout',
params: {value: {name: ''}}, params: {value: {name: ''}},
}); })
}}> }}>
Add workout Add workout
</Button> </Button>
@ -127,7 +127,7 @@ export default function EditPlan() {
)} )}
</View> </View>
</> </>
); )
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
@ -135,4 +135,4 @@ const styles = StyleSheet.create({
fontSize: 20, fontSize: 20,
marginBottom: MARGIN, marginBottom: MARGIN,
}, },
}); })

View File

@ -1,64 +1,64 @@
import {RouteProp, useNavigation, useRoute} from '@react-navigation/native'; import {RouteProp, useNavigation, useRoute} from '@react-navigation/native'
import {useCallback} from 'react'; import {useCallback} from 'react'
import {NativeModules, View} from 'react-native'; import {NativeModules, View} from 'react-native'
import {PADDING} from './constants'; import {PADDING} from './constants'
import {getNow, setRepo} from './db'; import {getNow, setRepo} from './db'
import GymSet from './gym-set'; import GymSet from './gym-set'
import {HomePageParams} from './home-page-params'; import {HomePageParams} from './home-page-params'
import {useSnackbar} from './MassiveSnack'; import {useSnackbar} from './MassiveSnack'
import SetForm from './SetForm'; import SetForm from './SetForm'
import StackHeader from './StackHeader'; import StackHeader from './StackHeader'
import {useSettings} from './use-settings'; import {useSettings} from './use-settings'
export default function EditSet() { export default function EditSet() {
const {params} = useRoute<RouteProp<HomePageParams, 'EditSet'>>(); const {params} = useRoute<RouteProp<HomePageParams, 'EditSet'>>()
const {set} = params; const {set} = params
const navigation = useNavigation(); const navigation = useNavigation()
const {toast} = useSnackbar(); const {toast} = useSnackbar()
const {settings} = useSettings(); const {settings} = useSettings()
const startTimer = useCallback( const startTimer = useCallback(
async (name: string) => { async (name: string) => {
if (!settings.alarm) return; if (!settings.alarm) return
const {minutes, seconds} = await setRepo.findOne({where: {name}}); const {minutes, seconds} = await setRepo.findOne({where: {name}})
const milliseconds = (minutes ?? 3) * 60 * 1000 + (seconds ?? 0) * 1000; const milliseconds = (minutes ?? 3) * 60 * 1000 + (seconds ?? 0) * 1000
NativeModules.AlarmModule.timer( NativeModules.AlarmModule.timer(
milliseconds, milliseconds,
!!settings.vibrate, !!settings.vibrate,
settings.sound, settings.sound,
!!settings.noSound, !!settings.noSound,
); )
}, },
[settings], [settings],
); )
const add = useCallback( const add = useCallback(
async (value: GymSet) => { async (value: GymSet) => {
startTimer(value.name); startTimer(value.name)
const [{now}] = await getNow(); const [{now}] = await getNow()
value.created = now; value.created = now
value.hidden = false; value.hidden = false
console.log(`${EditSet.name}.add`, {set: value}); console.log(`${EditSet.name}.add`, {set: value})
const result = await setRepo.save(value); const result = await setRepo.save(value)
console.log({result}); console.log({result})
if (!settings.notify) return; if (!settings.notify) return
if ( if (
value.weight > set.weight || value.weight > set.weight ||
(value.reps > set.reps && value.weight === set.weight) (value.reps > set.reps && value.weight === set.weight)
) )
toast("Great work King! That's a new record.", 3000); toast("Great work King! That's a new record.", 3000)
}, },
[startTimer, set, toast, settings], [startTimer, set, toast, settings],
); )
const save = useCallback( const save = useCallback(
async (value: GymSet) => { async (value: GymSet) => {
if (typeof set.id === 'number') await setRepo.save(value); if (typeof set.id === 'number') await setRepo.save(value)
else await add(value); else await add(value)
navigation.goBack(); navigation.goBack()
}, },
[add, set.id, navigation], [add, set.id, navigation],
); )
return ( return (
<> <>
@ -67,5 +67,5 @@ export default function EditSet() {
<SetForm save={save} set={set} /> <SetForm save={save} set={set} />
</View> </View>
</> </>
); )
} }

View File

@ -1,39 +1,39 @@
import {RouteProp, useNavigation, useRoute} from '@react-navigation/native'; import {RouteProp, useNavigation, useRoute} from '@react-navigation/native'
import {useCallback, useRef, useState} from 'react'; import {useCallback, useRef, useState} from 'react'
import {ScrollView, TextInput, View} from 'react-native'; import {ScrollView, TextInput, View} from 'react-native'
import DocumentPicker from 'react-native-document-picker'; import DocumentPicker from 'react-native-document-picker'
import {Button, Card, TouchableRipple} from 'react-native-paper'; import {Button, Card, TouchableRipple} from 'react-native-paper'
import {Like} from 'typeorm'; import {Like} from 'typeorm'
import ConfirmDialog from './ConfirmDialog'; import ConfirmDialog from './ConfirmDialog'
import {MARGIN, PADDING} from './constants'; import {MARGIN, PADDING} from './constants'
import {getNow, planRepo, setRepo} from './db'; import {getNow, planRepo, setRepo} from './db'
import MassiveInput from './MassiveInput'; import MassiveInput from './MassiveInput'
import {useSnackbar} from './MassiveSnack'; import {useSnackbar} from './MassiveSnack'
import StackHeader from './StackHeader'; import StackHeader from './StackHeader'
import {useSettings} from './use-settings'; import {useSettings} from './use-settings'
import {WorkoutsPageParams} from './WorkoutsPage'; import {WorkoutsPageParams} from './WorkoutsPage'
export default function EditWorkout() { export default function EditWorkout() {
const {params} = useRoute<RouteProp<WorkoutsPageParams, 'EditWorkout'>>(); const {params} = useRoute<RouteProp<WorkoutsPageParams, 'EditWorkout'>>()
const [removeImage, setRemoveImage] = useState(false); const [removeImage, setRemoveImage] = useState(false)
const [showRemove, setShowRemove] = useState(false); const [showRemove, setShowRemove] = useState(false)
const [name, setName] = useState(params.value.name); const [name, setName] = useState(params.value.name)
const [steps, setSteps] = useState(params.value.steps); const [steps, setSteps] = useState(params.value.steps)
const [uri, setUri] = useState(params.value.image); const [uri, setUri] = useState(params.value.image)
const [minutes, setMinutes] = useState( const [minutes, setMinutes] = useState(
params.value.minutes?.toString() ?? '3', params.value.minutes?.toString() ?? '3',
); )
const [seconds, setSeconds] = useState( const [seconds, setSeconds] = useState(
params.value.seconds?.toString() ?? '30', params.value.seconds?.toString() ?? '30',
); )
const [sets, setSets] = useState(params.value.sets?.toString() ?? '3'); const [sets, setSets] = useState(params.value.sets?.toString() ?? '3')
const {toast} = useSnackbar(); const {toast} = useSnackbar()
const navigation = useNavigation(); const navigation = useNavigation()
const setsRef = useRef<TextInput>(null); const setsRef = useRef<TextInput>(null)
const stepsRef = useRef<TextInput>(null); const stepsRef = useRef<TextInput>(null)
const minutesRef = useRef<TextInput>(null); const minutesRef = useRef<TextInput>(null)
const secondsRef = useRef<TextInput>(null); const secondsRef = useRef<TextInput>(null)
const {settings} = useSettings(); const {settings} = useSettings()
const update = async () => { const update = async () => {
await setRepo.update( await setRepo.update(
@ -46,18 +46,18 @@ export default function EditWorkout() {
steps, steps,
image: removeImage ? '' : uri, image: removeImage ? '' : uri,
}, },
); )
await planRepo.query( await planRepo.query(
`UPDATE plans `UPDATE plans
SET workouts = REPLACE(workouts, $1, $2) SET workouts = REPLACE(workouts, $1, $2)
WHERE workouts LIKE $3`, WHERE workouts LIKE $3`,
[params.value.name, name, `%${params.value.name}%`], [params.value.name, name, `%${params.value.name}%`],
); )
navigation.goBack(); navigation.goBack()
}; }
const add = async () => { const add = async () => {
const [{now}] = await getNow(); const [{now}] = await getNow()
await setRepo.save({ await setRepo.save({
name, name,
reps: 0, reps: 0,
@ -69,45 +69,45 @@ export default function EditWorkout() {
sets: sets ? +sets : 3, sets: sets ? +sets : 3,
steps, steps,
created: now, created: now,
}); })
navigation.goBack(); navigation.goBack()
}; }
const save = async () => { const save = async () => {
if (params.value.name) return update(); if (params.value.name) return update()
return add(); return add()
}; }
const changeImage = useCallback(async () => { const changeImage = useCallback(async () => {
const {fileCopyUri} = await DocumentPicker.pickSingle({ const {fileCopyUri} = await DocumentPicker.pickSingle({
type: 'image/*', type: 'image/*',
copyTo: 'documentDirectory', copyTo: 'documentDirectory',
}); })
if (fileCopyUri) setUri(fileCopyUri); if (fileCopyUri) setUri(fileCopyUri)
}, []); }, [])
const handleRemove = useCallback(async () => { const handleRemove = useCallback(async () => {
setUri(''); setUri('')
setRemoveImage(true); setRemoveImage(true)
setShowRemove(false); setShowRemove(false)
}, []); }, [])
const handleName = (value: string) => { const handleName = (value: string) => {
setName(value.replace(/,|'/g, '')); setName(value.replace(/,|'/g, ''))
if (value.match(/,|'/)) if (value.match(/,|'/))
toast('Commas and single quotes would break CSV exports', 6000); toast('Commas and single quotes would break CSV exports', 6000)
}; }
const handleSteps = (value: string) => { const handleSteps = (value: string) => {
setSteps(value.replace(/,|'/g, '')); setSteps(value.replace(/,|'/g, ''))
if (value.match(/,|'/)) if (value.match(/,|'/))
toast('Commas and single quotes would break CSV exports', 6000); toast('Commas and single quotes would break CSV exports', 6000)
}; }
const submitName = () => { const submitName = () => {
if (settings.steps) stepsRef.current?.focus(); if (settings.steps) stepsRef.current?.focus()
else setsRef.current?.focus(); else setsRef.current?.focus()
}; }
return ( return (
<> <>
@ -191,5 +191,5 @@ export default function EditWorkout() {
</ConfirmDialog> </ConfirmDialog>
</View> </View>
</> </>
); )
} }

View File

@ -1,9 +1,9 @@
import {createStackNavigator} from '@react-navigation/stack'; import {createStackNavigator} from '@react-navigation/stack'
import EditSet from './EditSet'; import EditSet from './EditSet'
import {HomePageParams} from './home-page-params'; import {HomePageParams} from './home-page-params'
import SetList from './SetList'; import SetList from './SetList'
const Stack = createStackNavigator<HomePageParams>(); const Stack = createStackNavigator<HomePageParams>()
export default function HomePage() { export default function HomePage() {
return ( return (
@ -12,5 +12,5 @@ export default function HomePage() {
<Stack.Screen name="Sets" component={SetList} /> <Stack.Screen name="Sets" component={SetList} />
<Stack.Screen name="EditSet" component={EditSet} /> <Stack.Screen name="EditSet" component={EditSet} />
</Stack.Navigator> </Stack.Navigator>
); )
} }

View File

@ -1,13 +1,13 @@
import {ComponentProps} from 'react'; import {ComponentProps} from 'react'
import {FAB} from 'react-native-paper'; import {FAB} from 'react-native-paper'
import {useColor} from './color'; import {useColor} from './color'
import {lightColors} from './colors'; import {lightColors} from './colors'
export default function MassiveFab(props: Partial<ComponentProps<typeof FAB>>) { export default function MassiveFab(props: Partial<ComponentProps<typeof FAB>>) {
const {color} = useColor(); const {color} = useColor()
const fabColor = lightColors.map(lightColor => lightColor.hex).includes(color) const fabColor = lightColors.map(lightColor => lightColor.hex).includes(color)
? 'black' ? 'black'
: undefined; : undefined
return ( return (
<FAB <FAB
@ -21,5 +21,5 @@ export default function MassiveFab(props: Partial<ComponentProps<typeof FAB>>) {
}} }}
{...props} {...props}
/> />
); )
} }

View File

@ -1,15 +1,15 @@
import {ComponentProps, Ref} from 'react'; import {ComponentProps, Ref} from 'react'
import {TextInput} from 'react-native-paper'; import {TextInput} from 'react-native-paper'
import {CombinedDefaultTheme} from './App'; import {CombinedDefaultTheme} from './App'
import {MARGIN} from './constants'; import {MARGIN} from './constants'
import useDark from './use-dark'; import useDark from './use-dark'
export default function MassiveInput( export default function MassiveInput(
props: Partial<ComponentProps<typeof TextInput>> & { props: Partial<ComponentProps<typeof TextInput>> & {
innerRef?: Ref<any>; innerRef?: Ref<any>
}, },
) { ) {
const dark = useDark(); const dark = useDark()
return ( return (
<TextInput <TextInput
@ -21,5 +21,5 @@ export default function MassiveInput(
blurOnSubmit={false} blurOnSubmit={false}
{...props} {...props}
/> />
); )
} }

View File

@ -1,31 +1,31 @@
import {createContext, useContext, useState} from 'react'; import {createContext, useContext, useState} from 'react'
import {Snackbar} from 'react-native-paper'; import {Snackbar} from 'react-native-paper'
import {CombinedDarkTheme, CombinedDefaultTheme} from './App'; import {CombinedDarkTheme, CombinedDefaultTheme} from './App'
import useDark from './use-dark'; import useDark from './use-dark'
export const SnackbarContext = createContext<{ export const SnackbarContext = createContext<{
toast: (value: string, timeout: number) => void; toast: (value: string, timeout: number) => void
}>({toast: () => null}); }>({toast: () => null})
export const useSnackbar = () => { export const useSnackbar = () => {
return useContext(SnackbarContext); return useContext(SnackbarContext)
}; }
export default function MassiveSnack({ export default function MassiveSnack({
children, children,
}: { }: {
children?: JSX.Element[] | JSX.Element; children?: JSX.Element[] | JSX.Element
}) { }) {
const [snackbar, setSnackbar] = useState(''); const [snackbar, setSnackbar] = useState('')
const [timeoutId, setTimeoutId] = useState(0); const [timeoutId, setTimeoutId] = useState(0)
const dark = useDark(); const dark = useDark()
const toast = (value: string, timeout: number) => { const toast = (value: string, timeout: number) => {
setSnackbar(value); setSnackbar(value)
clearTimeout(timeoutId); clearTimeout(timeoutId)
const id = setTimeout(() => setSnackbar(''), timeout); const id = setTimeout(() => setSnackbar(''), timeout)
setTimeoutId(id); setTimeoutId(id)
}; }
return ( return (
<> <>
@ -45,5 +45,5 @@ export default function MassiveSnack({
{snackbar} {snackbar}
</Snackbar> </Snackbar>
</> </>
); )
} }

View File

@ -1,7 +1,7 @@
import {StyleSheet, View} from 'react-native'; import {StyleSheet, View} from 'react-native'
import {Searchbar} from 'react-native-paper'; import {Searchbar} from 'react-native-paper'
import {PADDING} from './constants'; import {PADDING} from './constants'
import MassiveFab from './MassiveFab'; import MassiveFab from './MassiveFab'
export default function Page({ export default function Page({
onAdd, onAdd,
@ -9,10 +9,10 @@ export default function Page({
term, term,
search, search,
}: { }: {
children: JSX.Element | JSX.Element[]; children: JSX.Element | JSX.Element[]
onAdd?: () => void; onAdd?: () => void
term: string; term: string
search: (value: string) => void; search: (value: string) => void
}) { }) {
return ( return (
<View style={styles.container}> <View style={styles.container}>
@ -26,7 +26,7 @@ export default function Page({
{children} {children}
{onAdd && <MassiveFab onPress={onAdd} />} {onAdd && <MassiveFab onPress={onAdd} />}
</View> </View>
); )
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
@ -34,4 +34,4 @@ const styles = StyleSheet.create({
flexGrow: 1, flexGrow: 1,
padding: PADDING, padding: PADDING,
}, },
}); })

View File

@ -2,60 +2,60 @@ import {
NavigationProp, NavigationProp,
useFocusEffect, useFocusEffect,
useNavigation, useNavigation,
} from '@react-navigation/native'; } from '@react-navigation/native'
import {useCallback, useMemo, useState} from 'react'; import {useCallback, useMemo, useState} from 'react'
import {GestureResponderEvent, Text} from 'react-native'; import {GestureResponderEvent, Text} from 'react-native'
import {Divider, List, Menu} from 'react-native-paper'; import {Divider, List, Menu} from 'react-native-paper'
import {getBestSet} from './best.service'; import {getBestSet} from './best.service'
import {planRepo} from './db'; import {planRepo} from './db'
import {Plan} from './plan'; import {Plan} from './plan'
import {PlanPageParams} from './plan-page-params'; import {PlanPageParams} from './plan-page-params'
import {DAYS} from './time'; import {DAYS} from './time'
export default function PlanItem({ export default function PlanItem({
item, item,
onRemove, onRemove,
}: { }: {
item: Plan; item: Plan
onRemove: () => void; onRemove: () => void
}) { }) {
const [show, setShow] = useState(false); const [show, setShow] = useState(false)
const [anchor, setAnchor] = useState({x: 0, y: 0}); const [anchor, setAnchor] = useState({x: 0, y: 0})
const [today, setToday] = useState<string>(); const [today, setToday] = useState<string>()
const days = useMemo(() => item.days.split(','), [item.days]); const days = useMemo(() => item.days.split(','), [item.days])
const navigation = useNavigation<NavigationProp<PlanPageParams>>(); const navigation = useNavigation<NavigationProp<PlanPageParams>>()
useFocusEffect( useFocusEffect(
useCallback(() => { useCallback(() => {
const newToday = DAYS[new Date().getDay()]; const newToday = DAYS[new Date().getDay()]
setToday(newToday); setToday(newToday)
}, []), }, []),
); )
const remove = useCallback(async () => { const remove = useCallback(async () => {
if (typeof item.id === 'number') await planRepo.delete(item.id); if (typeof item.id === 'number') await planRepo.delete(item.id)
setShow(false); setShow(false)
onRemove(); onRemove()
}, [setShow, item.id, onRemove]); }, [setShow, item.id, onRemove])
const start = useCallback(async () => { const start = useCallback(async () => {
console.log(`${PlanItem.name}.start:`, {item}); console.log(`${PlanItem.name}.start:`, {item})
setShow(false); setShow(false)
navigation.navigate('StartPlan', {plan: item}); navigation.navigate('StartPlan', {plan: item})
}, [item, navigation]); }, [item, navigation])
const longPress = useCallback( const longPress = useCallback(
(e: GestureResponderEvent) => { (e: GestureResponderEvent) => {
setAnchor({x: e.nativeEvent.pageX, y: e.nativeEvent.pageY}); setAnchor({x: e.nativeEvent.pageX, y: e.nativeEvent.pageY})
setShow(true); setShow(true)
}, },
[setAnchor, setShow], [setAnchor, setShow],
); )
const edit = useCallback(() => { const edit = useCallback(() => {
setShow(false); setShow(false)
navigation.navigate('EditPlan', {plan: item}); navigation.navigate('EditPlan', {plan: item})
}, [navigation, item]); }, [navigation, item])
const title = useMemo( const title = useMemo(
() => () =>
@ -72,12 +72,12 @@ export default function PlanItem({
</Text> </Text>
)), )),
[days, today], [days, today],
); )
const description = useMemo( const description = useMemo(
() => item.workouts.replace(/,/g, ', '), () => item.workouts.replace(/,/g, ', '),
[item.workouts], [item.workouts],
); )
return ( return (
<> <>
@ -95,5 +95,5 @@ export default function PlanItem({
)} )}
/> />
</> </>
); )
} }

View File

@ -2,54 +2,54 @@ import {
NavigationProp, NavigationProp,
useFocusEffect, useFocusEffect,
useNavigation, useNavigation,
} from '@react-navigation/native'; } from '@react-navigation/native'
import {useCallback, useState} from 'react'; import {useCallback, useState} from 'react'
import {FlatList} from 'react-native'; import {FlatList} from 'react-native'
import {List} from 'react-native-paper'; import {List} from 'react-native-paper'
import {Like} from 'typeorm'; import {Like} from 'typeorm'
import {planRepo} from './db'; import {planRepo} from './db'
import DrawerHeader from './DrawerHeader'; import DrawerHeader from './DrawerHeader'
import Page from './Page'; import Page from './Page'
import {Plan} from './plan'; import {Plan} from './plan'
import {PlanPageParams} from './plan-page-params'; import {PlanPageParams} from './plan-page-params'
import PlanItem from './PlanItem'; import PlanItem from './PlanItem'
export default function PlanList() { export default function PlanList() {
const [term, setTerm] = useState(''); const [term, setTerm] = useState('')
const [plans, setPlans] = useState<Plan[]>(); const [plans, setPlans] = useState<Plan[]>()
const navigation = useNavigation<NavigationProp<PlanPageParams>>(); const navigation = useNavigation<NavigationProp<PlanPageParams>>()
const refresh = useCallback(async (value: string) => { const refresh = useCallback(async (value: string) => {
planRepo planRepo
.find({ .find({
where: [{days: Like(`%${value}%`)}, {workouts: Like(`%${value}%`)}], where: [{days: Like(`%${value}%`)}, {workouts: Like(`%${value}%`)}],
}) })
.then(setPlans); .then(setPlans)
}, []); }, [])
useFocusEffect( useFocusEffect(
useCallback(() => { useCallback(() => {
refresh(term); refresh(term)
}, [refresh, term]), }, [refresh, term]),
); )
const search = useCallback( const search = useCallback(
(value: string) => { (value: string) => {
setTerm(value); setTerm(value)
refresh(value); refresh(value)
}, },
[refresh], [refresh],
); )
const renderItem = useCallback( const renderItem = useCallback(
({item}: {item: Plan}) => ( ({item}: {item: Plan}) => (
<PlanItem item={item} key={item.id} onRemove={() => refresh(term)} /> <PlanItem item={item} key={item.id} onRemove={() => refresh(term)} />
), ),
[refresh, term], [refresh, term],
); )
const onAdd = () => const onAdd = () =>
navigation.navigate('EditPlan', {plan: {days: '', workouts: ''}}); navigation.navigate('EditPlan', {plan: {days: '', workouts: ''}})
return ( return (
<> <>
@ -70,5 +70,5 @@ export default function PlanList() {
)} )}
</Page> </Page>
</> </>
); )
} }

View File

@ -1,10 +1,10 @@
import {createStackNavigator} from '@react-navigation/stack'; import {createStackNavigator} from '@react-navigation/stack'
import EditPlan from './EditPlan'; import EditPlan from './EditPlan'
import {PlanPageParams} from './plan-page-params'; import {PlanPageParams} from './plan-page-params'
import PlanList from './PlanList'; import PlanList from './PlanList'
import StartPlan from './StartPlan'; import StartPlan from './StartPlan'
const Stack = createStackNavigator<PlanPageParams>(); const Stack = createStackNavigator<PlanPageParams>()
export default function PlanPage() { export default function PlanPage() {
return ( return (
@ -14,5 +14,5 @@ export default function PlanPage() {
<Stack.Screen name="EditPlan" component={EditPlan} /> <Stack.Screen name="EditPlan" component={EditPlan} />
<Stack.Screen name="StartPlan" component={StartPlan} /> <Stack.Screen name="StartPlan" component={StartPlan} />
</Stack.Navigator> </Stack.Navigator>
); )
} }

View File

@ -1,18 +1,18 @@
import {createDrawerNavigator} from '@react-navigation/drawer'; import {createDrawerNavigator} from '@react-navigation/drawer'
import {IconButton} from 'react-native-paper'; import {IconButton} from 'react-native-paper'
import BestPage from './BestPage'; import BestPage from './BestPage'
import {DrawerParamList} from './drawer-param-list'; import {DrawerParamList} from './drawer-param-list'
import HomePage from './HomePage'; import HomePage from './HomePage'
import PlanPage from './PlanPage'; import PlanPage from './PlanPage'
import Route from './route'; import Route from './route'
import SettingsPage from './SettingsPage'; import SettingsPage from './SettingsPage'
import useDark from './use-dark'; import useDark from './use-dark'
import WorkoutsPage from './WorkoutsPage'; import WorkoutsPage from './WorkoutsPage'
const Drawer = createDrawerNavigator<DrawerParamList>(); const Drawer = createDrawerNavigator<DrawerParamList>()
export default function Routes() { export default function Routes() {
const dark = useDark(); const dark = useDark()
const routes: Route[] = [ const routes: Route[] = [
{name: 'Home', component: HomePage, icon: 'home'}, {name: 'Home', component: HomePage, icon: 'home'},
@ -20,7 +20,7 @@ export default function Routes() {
{name: 'Best', component: BestPage, icon: 'insights'}, {name: 'Best', component: BestPage, icon: 'insights'},
{name: 'Workouts', component: WorkoutsPage, icon: 'fitness-center'}, {name: 'Workouts', component: WorkoutsPage, icon: 'fitness-center'},
{name: 'Settings', component: SettingsPage, icon: 'settings'}, {name: 'Settings', component: SettingsPage, icon: 'settings'},
]; ]
return ( return (
<Drawer.Navigator <Drawer.Navigator
@ -40,5 +40,5 @@ export default function Routes() {
/> />
))} ))}
</Drawer.Navigator> </Drawer.Navigator>
); )
} }

View File

@ -1,47 +1,47 @@
import {useCallback, useRef, useState} from 'react'; import {useCallback, useRef, useState} from 'react'
import {TextInput, View} from 'react-native'; import {TextInput, View} from 'react-native'
import DocumentPicker from 'react-native-document-picker'; import DocumentPicker from 'react-native-document-picker'
import {Button, Card, TouchableRipple} from 'react-native-paper'; import {Button, Card, TouchableRipple} from 'react-native-paper'
import ConfirmDialog from './ConfirmDialog'; import ConfirmDialog from './ConfirmDialog'
import {MARGIN} from './constants'; import {MARGIN} from './constants'
import {setRepo} from './db'; import {setRepo} from './db'
import GymSet from './gym-set'; import GymSet from './gym-set'
import MassiveInput from './MassiveInput'; import MassiveInput from './MassiveInput'
import {useSnackbar} from './MassiveSnack'; import {useSnackbar} from './MassiveSnack'
import {useSettings} from './use-settings'; import {useSettings} from './use-settings'
export default function SetForm({ export default function SetForm({
save, save,
set, set,
}: { }: {
set: GymSet; set: GymSet
save: (set: GymSet) => void; save: (set: GymSet) => void
}) { }) {
const [name, setName] = useState(set.name); const [name, setName] = useState(set.name)
const [reps, setReps] = useState(set.reps.toString()); const [reps, setReps] = useState(set.reps.toString())
const [weight, setWeight] = useState(set.weight.toString()); const [weight, setWeight] = useState(set.weight.toString())
const [newImage, setNewImage] = useState(set.image); const [newImage, setNewImage] = useState(set.image)
const [unit, setUnit] = useState(set.unit); const [unit, setUnit] = useState(set.unit)
const [showRemove, setShowRemove] = useState(false); const [showRemove, setShowRemove] = useState(false)
const [selection, setSelection] = useState({ const [selection, setSelection] = useState({
start: 0, start: 0,
end: set.reps.toString().length, end: set.reps.toString().length,
}); })
const [removeImage, setRemoveImage] = useState(false); const [removeImage, setRemoveImage] = useState(false)
const {toast} = useSnackbar(); const {toast} = useSnackbar()
const {settings} = useSettings(); const {settings} = useSettings()
const weightRef = useRef<TextInput>(null); const weightRef = useRef<TextInput>(null)
const repsRef = useRef<TextInput>(null); const repsRef = useRef<TextInput>(null)
const unitRef = useRef<TextInput>(null); const unitRef = useRef<TextInput>(null)
const handleSubmit = async () => { const handleSubmit = async () => {
console.log(`${SetForm.name}.handleSubmit:`, {set, uri: newImage, name}); console.log(`${SetForm.name}.handleSubmit:`, {set, uri: newImage, name})
if (!name) return; if (!name) return
let image = newImage; let image = newImage
if (!newImage && !removeImage) if (!newImage && !removeImage)
image = await setRepo.findOne({where: {name}}).then(s => s?.image); image = await setRepo.findOne({where: {name}}).then(s => s?.image)
console.log(`${SetForm.name}.handleSubmit:`, {image}); console.log(`${SetForm.name}.handleSubmit:`, {image})
save({ save({
name, name,
reps: Number(reps), reps: Number(reps),
@ -53,34 +53,34 @@ export default function SetForm({
seconds: Number(set.seconds ?? 30), seconds: Number(set.seconds ?? 30),
sets: set.sets ?? 3, sets: set.sets ?? 3,
hidden: false, hidden: false,
}); })
}; }
const handleName = (value: string) => { const handleName = (value: string) => {
setName(value.replace(/,|'/g, '')); setName(value.replace(/,|'/g, ''))
if (value.match(/,|'/)) if (value.match(/,|'/))
toast('Commas and single quotes would break CSV exports', 6000); toast('Commas and single quotes would break CSV exports', 6000)
}; }
const handleUnit = (value: string) => { const handleUnit = (value: string) => {
setUnit(value.replace(/,|'/g, '')); setUnit(value.replace(/,|'/g, ''))
if (value.match(/,|'/)) if (value.match(/,|'/))
toast('Commas and single quotes would break CSV exports', 6000); toast('Commas and single quotes would break CSV exports', 6000)
}; }
const changeImage = useCallback(async () => { const changeImage = useCallback(async () => {
const {fileCopyUri} = await DocumentPicker.pickSingle({ const {fileCopyUri} = await DocumentPicker.pickSingle({
type: 'image/*', type: 'image/*',
copyTo: 'documentDirectory', copyTo: 'documentDirectory',
}); })
if (fileCopyUri) setNewImage(fileCopyUri); if (fileCopyUri) setNewImage(fileCopyUri)
}, []); }, [])
const handleRemove = useCallback(async () => { const handleRemove = useCallback(async () => {
setNewImage(''); setNewImage('')
setRemoveImage(true); setRemoveImage(true)
setShowRemove(false); setShowRemove(false)
}, []); }, [])
return ( return (
<> <>
@ -156,5 +156,5 @@ export default function SetForm({
Are you sure you want to remove the image? Are you sure you want to remove the image?
</ConfirmDialog> </ConfirmDialog>
</> </>
); )
} }

View File

@ -1,47 +1,47 @@
import {NavigationProp, useNavigation} from '@react-navigation/native'; import {NavigationProp, useNavigation} from '@react-navigation/native'
import {useCallback, useState} from 'react'; import {useCallback, useState} from 'react'
import {GestureResponderEvent, Image} from 'react-native'; import {GestureResponderEvent, Image} from 'react-native'
import {Divider, List, Menu, Text} from 'react-native-paper'; import {Divider, List, Menu, Text} from 'react-native-paper'
import {setRepo} from './db'; import {setRepo} from './db'
import GymSet from './gym-set'; import GymSet from './gym-set'
import {HomePageParams} from './home-page-params'; import {HomePageParams} from './home-page-params'
import {format} from './time'; import {format} from './time'
import useDark from './use-dark'; import useDark from './use-dark'
import {useSettings} from './use-settings'; import {useSettings} from './use-settings'
export default function SetItem({ export default function SetItem({
item, item,
onRemove, onRemove,
}: { }: {
item: GymSet; item: GymSet
onRemove: () => void; onRemove: () => void
}) { }) {
const [showMenu, setShowMenu] = useState(false); const [showMenu, setShowMenu] = useState(false)
const [anchor, setAnchor] = useState({x: 0, y: 0}); const [anchor, setAnchor] = useState({x: 0, y: 0})
const {settings} = useSettings(); const {settings} = useSettings()
const dark = useDark(); const dark = useDark()
const navigation = useNavigation<NavigationProp<HomePageParams>>(); const navigation = useNavigation<NavigationProp<HomePageParams>>()
const remove = useCallback(async () => { const remove = useCallback(async () => {
if (typeof item.id === 'number') await setRepo.delete(item.id); if (typeof item.id === 'number') await setRepo.delete(item.id)
setShowMenu(false); setShowMenu(false)
onRemove(); onRemove()
}, [setShowMenu, onRemove, item.id]); }, [setShowMenu, onRemove, item.id])
const copy = useCallback(() => { const copy = useCallback(() => {
const set: GymSet = {...item}; const set: GymSet = {...item}
delete set.id; delete set.id
setShowMenu(false); setShowMenu(false)
navigation.navigate('EditSet', {set}); navigation.navigate('EditSet', {set})
}, [navigation, item]); }, [navigation, item])
const longPress = useCallback( const longPress = useCallback(
(e: GestureResponderEvent) => { (e: GestureResponderEvent) => {
setAnchor({x: e.nativeEvent.pageX, y: e.nativeEvent.pageY}); setAnchor({x: e.nativeEvent.pageX, y: e.nativeEvent.pageY})
setShowMenu(true); setShowMenu(true)
}, },
[setShowMenu, setAnchor], [setShowMenu, setAnchor],
); )
return ( return (
<> <>
@ -79,5 +79,5 @@ export default function SetItem({
)} )}
/> />
</> </>
); )
} }

View File

@ -2,29 +2,29 @@ import {
NavigationProp, NavigationProp,
useFocusEffect, useFocusEffect,
useNavigation, useNavigation,
} from '@react-navigation/native'; } from '@react-navigation/native'
import React, {useCallback, useEffect, useState} from 'react'; import React, {useCallback, useEffect, useState} from 'react'
import {FlatList} from 'react-native'; import {FlatList} from 'react-native'
import {List} from 'react-native-paper'; import {List} from 'react-native-paper'
import {Like} from 'typeorm'; import {Like} from 'typeorm'
import {getNow, setRepo} from './db'; import {getNow, setRepo} from './db'
import DrawerHeader from './DrawerHeader'; import DrawerHeader from './DrawerHeader'
import GymSet from './gym-set'; import GymSet from './gym-set'
import {HomePageParams} from './home-page-params'; import {HomePageParams} from './home-page-params'
import Page from './Page'; import Page from './Page'
import SetItem from './SetItem'; import SetItem from './SetItem'
const limit = 15; const limit = 15
export default function SetList() { export default function SetList() {
const [sets, setSets] = useState<GymSet[]>([]); const [sets, setSets] = useState<GymSet[]>([])
const [set, setSet] = useState<GymSet>(); const [set, setSet] = useState<GymSet>()
const [offset, setOffset] = useState(0); const [offset, setOffset] = useState(0)
const [term, setTerm] = useState(''); const [term, setTerm] = useState('')
const [end, setEnd] = useState(false); const [end, setEnd] = useState(false)
const navigation = useNavigation<NavigationProp<HomePageParams>>(); const navigation = useNavigation<NavigationProp<HomePageParams>>()
useEffect(() => console.log({sets}), [sets]); useEffect(() => console.log({sets}), [sets])
const refresh = useCallback(async (value: string) => { const refresh = useCallback(async (value: string) => {
const newSets = await setRepo.find({ const newSets = await setRepo.find({
@ -32,47 +32,47 @@ export default function SetList() {
take: limit, take: limit,
skip: 0, skip: 0,
order: {created: 'DESC'}, order: {created: 'DESC'},
}); })
setSet(newSets[0]); setSet(newSets[0])
if (newSets.length === 0) return setSets([]); if (newSets.length === 0) return setSets([])
setSets(newSets); setSets(newSets)
setOffset(0); setOffset(0)
setEnd(false); setEnd(false)
}, []); }, [])
useFocusEffect( useFocusEffect(
useCallback(() => { useCallback(() => {
refresh(term); refresh(term)
}, [refresh, term]), }, [refresh, term]),
); )
const renderItem = useCallback( const renderItem = useCallback(
({item}: {item: GymSet}) => ( ({item}: {item: GymSet}) => (
<SetItem item={item} key={item.id} onRemove={() => refresh(term)} /> <SetItem item={item} key={item.id} onRemove={() => refresh(term)} />
), ),
[refresh, term], [refresh, term],
); )
const next = useCallback(async () => { const next = useCallback(async () => {
if (end) return; if (end) return
const newOffset = offset + limit; const newOffset = offset + limit
console.log(`${SetList.name}.next:`, {offset, newOffset, term}); console.log(`${SetList.name}.next:`, {offset, newOffset, term})
const newSets = await setRepo.find({ const newSets = await setRepo.find({
where: {name: Like(`%${term}%`), hidden: 0 as any}, where: {name: Like(`%${term}%`), hidden: 0 as any},
take: limit, take: limit,
skip: newOffset, skip: newOffset,
order: {created: 'DESC'}, order: {created: 'DESC'},
}); })
if (newSets.length === 0) return setEnd(true); if (newSets.length === 0) return setEnd(true)
if (!sets) return; if (!sets) return
setSets([...sets, ...newSets]); setSets([...sets, ...newSets])
if (newSets.length < limit) return setEnd(true); if (newSets.length < limit) return setEnd(true)
setOffset(newOffset); setOffset(newOffset)
}, [term, end, offset, sets]); }, [term, end, offset, sets])
const onAdd = useCallback(async () => { const onAdd = useCallback(async () => {
console.log(`${SetList.name}.onAdd`, {set}); console.log(`${SetList.name}.onAdd`, {set})
const [{now}] = await getNow(); const [{now}] = await getNow()
navigation.navigate('EditSet', { navigation.navigate('EditSet', {
set: set || { set: set || {
hidden: false, hidden: false,
@ -84,16 +84,16 @@ export default function SetList() {
weight: 0, weight: 0,
created: now, created: now,
}, },
}); })
}, [navigation, set]); }, [navigation, set])
const search = useCallback( const search = useCallback(
(value: string) => { (value: string) => {
setTerm(value); setTerm(value)
refresh(value); refresh(value)
}, },
[refresh], [refresh],
); )
return ( return (
<> <>
@ -114,5 +114,5 @@ export default function SetList() {
)} )}
</Page> </Page>
</> </>
); )
} }

View File

@ -1,27 +1,27 @@
import {Picker} from '@react-native-picker/picker'; import {Picker} from '@react-native-picker/picker'
import {useFocusEffect} from '@react-navigation/native'; import {useFocusEffect} from '@react-navigation/native'
import {useCallback, useEffect, useState} from 'react'; import {useCallback, useEffect, useState} from 'react'
import {NativeModules, ScrollView} from 'react-native'; import {NativeModules, ScrollView} from 'react-native'
import DocumentPicker from 'react-native-document-picker'; import DocumentPicker from 'react-native-document-picker'
import {Button} from 'react-native-paper'; import {Button} from 'react-native-paper'
import {useColor} from './color'; import {useColor} from './color'
import {darkColors, lightColors} from './colors'; import {darkColors, lightColors} from './colors'
import ConfirmDialog from './ConfirmDialog'; import ConfirmDialog from './ConfirmDialog'
import {MARGIN} from './constants'; import {MARGIN} from './constants'
import {settingsRepo} from './db'; import {settingsRepo} from './db'
import DrawerHeader from './DrawerHeader'; import DrawerHeader from './DrawerHeader'
import Input from './input'; import Input from './input'
import {useSnackbar} from './MassiveSnack'; import {useSnackbar} from './MassiveSnack'
import Page from './Page'; import Page from './Page'
import Settings from './settings'; import Settings from './settings'
import Switch from './Switch'; import Switch from './Switch'
import {useSettings} from './use-settings'; import {useSettings} from './use-settings'
export default function SettingsPage() { export default function SettingsPage() {
const [battery, setBattery] = useState(false); const [battery, setBattery] = useState(false)
const [ignoring, setIgnoring] = useState(false); const [ignoring, setIgnoring] = useState(false)
const [term, setTerm] = useState(''); const [term, setTerm] = useState('')
const {settings, setSettings} = useSettings(); const {settings, setSettings} = useSettings()
const { const {
vibrate, vibrate,
sound, sound,
@ -34,120 +34,120 @@ export default function SettingsPage() {
theme, theme,
alarm, alarm,
noSound, noSound,
} = settings; } = settings
const {color, setColor} = useColor(); const {color, setColor} = useColor()
const {toast} = useSnackbar(); const {toast} = useSnackbar()
useEffect(() => { useEffect(() => {
console.log(`${SettingsPage.name}.useEffect:`, {settings}); console.log(`${SettingsPage.name}.useEffect:`, {settings})
}, [settings]); }, [settings])
useFocusEffect( useFocusEffect(
useCallback(() => { useCallback(() => {
NativeModules.AlarmModule.ignoringBattery(setIgnoring); NativeModules.AlarmModule.ignoringBattery(setIgnoring)
}, []), }, []),
); )
const update = useCallback( const update = useCallback(
(value: boolean, field: keyof Settings) => { (value: boolean, field: keyof Settings) => {
settingsRepo.update({}, {[field]: value}); settingsRepo.update({}, {[field]: value})
setSettings({...settings, [field]: value}); setSettings({...settings, [field]: value})
}, },
[settings, setSettings], [settings, setSettings],
); )
const changeAlarmEnabled = useCallback( const changeAlarmEnabled = useCallback(
(enabled: boolean) => { (enabled: boolean) => {
if (enabled) toast('Timers will now run after each set.', 4000); if (enabled) toast('Timers will now run after each set.', 4000)
else toast('Stopped timers running after each set.', 4000); else toast('Stopped timers running after each set.', 4000)
if (enabled && !ignoring) setBattery(true); if (enabled && !ignoring) setBattery(true)
update(enabled, 'alarm'); update(enabled, 'alarm')
}, },
[setBattery, ignoring, toast, update], [setBattery, ignoring, toast, update],
); )
const changeVibrate = useCallback( const changeVibrate = useCallback(
(enabled: boolean) => { (enabled: boolean) => {
if (enabled) toast('When a timer completes, vibrate your phone.', 4000); if (enabled) toast('When a timer completes, vibrate your phone.', 4000)
else toast('Stop vibrating at the end of timers.', 4000); else toast('Stop vibrating at the end of timers.', 4000)
update(enabled, 'vibrate'); update(enabled, 'vibrate')
}, },
[toast, update], [toast, update],
); )
const changeSound = useCallback(async () => { const changeSound = useCallback(async () => {
const {fileCopyUri} = await DocumentPicker.pickSingle({ const {fileCopyUri} = await DocumentPicker.pickSingle({
type: 'audio/*', type: 'audio/*',
copyTo: 'documentDirectory', copyTo: 'documentDirectory',
}); })
if (!fileCopyUri) return; if (!fileCopyUri) return
settingsRepo.update({}, {sound: fileCopyUri}); settingsRepo.update({}, {sound: fileCopyUri})
setSettings({...settings, sound: fileCopyUri}); setSettings({...settings, sound: fileCopyUri})
toast('This song will now play after rest timers complete.', 4000); toast('This song will now play after rest timers complete.', 4000)
}, [toast, setSettings, settings]); }, [toast, setSettings, settings])
const changeNotify = useCallback( const changeNotify = useCallback(
(enabled: boolean) => { (enabled: boolean) => {
update(enabled, 'notify'); update(enabled, 'notify')
if (enabled) toast('Show when a set is a new record.', 4000); if (enabled) toast('Show when a set is a new record.', 4000)
else toast('Stopped showing notifications for new records.', 4000); else toast('Stopped showing notifications for new records.', 4000)
}, },
[toast, update], [toast, update],
); )
const changeImages = useCallback( const changeImages = useCallback(
(enabled: boolean) => { (enabled: boolean) => {
update(enabled, 'images'); update(enabled, 'images')
if (enabled) toast('Show images for sets.', 4000); if (enabled) toast('Show images for sets.', 4000)
else toast('Stopped showing images for sets.', 4000); else toast('Stopped showing images for sets.', 4000)
}, },
[toast, update], [toast, update],
); )
const changeUnit = useCallback( const changeUnit = useCallback(
(enabled: boolean) => { (enabled: boolean) => {
update(enabled, 'showUnit'); update(enabled, 'showUnit')
if (enabled) toast('Show option to select unit for sets.', 4000); if (enabled) toast('Show option to select unit for sets.', 4000)
else toast('Hid unit option for sets.', 4000); else toast('Hid unit option for sets.', 4000)
}, },
[toast, update], [toast, update],
); )
const changeSteps = useCallback( const changeSteps = useCallback(
(enabled: boolean) => { (enabled: boolean) => {
update(enabled, 'steps'); update(enabled, 'steps')
if (enabled) toast('Show steps for a workout.', 4000); if (enabled) toast('Show steps for a workout.', 4000)
else toast('Stopped showing steps for workouts.', 4000); else toast('Stopped showing steps for workouts.', 4000)
}, },
[toast, update], [toast, update],
); )
const changeShowDate = useCallback( const changeShowDate = useCallback(
(enabled: boolean) => { (enabled: boolean) => {
update(enabled, 'showDate'); update(enabled, 'showDate')
if (enabled) toast('Show date for sets by default.', 4000); if (enabled) toast('Show date for sets by default.', 4000)
else toast('Stopped showing date for sets by default.', 4000); else toast('Stopped showing date for sets by default.', 4000)
}, },
[toast, update], [toast, update],
); )
const changeShowSets = useCallback( const changeShowSets = useCallback(
(enabled: boolean) => { (enabled: boolean) => {
update(enabled, 'showSets'); update(enabled, 'showSets')
if (enabled) toast('Show maximum sets for workouts.', 4000); if (enabled) toast('Show maximum sets for workouts.', 4000)
else toast('Stopped showing maximum sets for workouts.', 4000); else toast('Stopped showing maximum sets for workouts.', 4000)
}, },
[toast, update], [toast, update],
); )
const changeNoSound = useCallback( const changeNoSound = useCallback(
(enabled: boolean) => { (enabled: boolean) => {
update(enabled, 'noSound'); update(enabled, 'noSound')
if (enabled) toast('Disable sound on rest timer alarms.', 4000); if (enabled) toast('Disable sound on rest timer alarms.', 4000)
else toast('Enabled sound for rest timer alarms.', 4000); else toast('Enabled sound for rest timer alarms.', 4000)
}, },
[toast, update], [toast, update],
); )
const switches: Input<boolean>[] = [ const switches: Input<boolean>[] = [
{name: 'Rest timers', value: alarm, onChange: changeAlarmEnabled}, {name: 'Rest timers', value: alarm, onChange: changeAlarmEnabled},
@ -159,23 +159,23 @@ export default function SettingsPage() {
{name: 'Show steps', value: steps, onChange: changeSteps}, {name: 'Show steps', value: steps, onChange: changeSteps},
{name: 'Show date', value: showDate, onChange: changeShowDate}, {name: 'Show date', value: showDate, onChange: changeShowDate},
{name: 'Show sets', value: showSets, onChange: changeShowSets}, {name: 'Show sets', value: showSets, onChange: changeShowSets},
]; ]
const changeTheme = useCallback( const changeTheme = useCallback(
(value: string) => { (value: string) => {
settingsRepo.update({}, {theme: value}); settingsRepo.update({}, {theme: value})
setSettings({...settings, theme: value}); setSettings({...settings, theme: value})
}, },
[settings, setSettings], [settings, setSettings],
); )
const changeDate = useCallback( const changeDate = useCallback(
(value: string) => { (value: string) => {
settingsRepo.update({}, {date: value}); settingsRepo.update({}, {date: value})
setSettings({...settings, date: value as any}); setSettings({...settings, date: value as any})
}, },
[settings, setSettings], [settings, setSettings],
); )
return ( return (
<> <>
@ -256,12 +256,12 @@ export default function SettingsPage() {
show={battery} show={battery}
setShow={setBattery} setShow={setBattery}
onOk={() => { onOk={() => {
NativeModules.AlarmModule.ignoreBattery(); NativeModules.AlarmModule.ignoreBattery()
setBattery(false); setBattery(false)
}}> }}>
Disable battery optimizations for Massive to use rest timers. Disable battery optimizations for Massive to use rest timers.
</ConfirmDialog> </ConfirmDialog>
</Page> </Page>
</> </>
); )
} }

View File

@ -1,13 +1,13 @@
import {useNavigation} from '@react-navigation/native'; import {useNavigation} from '@react-navigation/native'
import Share from 'react-native-share'; import Share from 'react-native-share'
import {FileSystem} from 'react-native-file-access'; import {FileSystem} from 'react-native-file-access'
import {Appbar, IconButton} from 'react-native-paper'; import {Appbar, IconButton} from 'react-native-paper'
import {captureScreen} from 'react-native-view-shot'; import {captureScreen} from 'react-native-view-shot'
import useDark from './use-dark'; import useDark from './use-dark'
export default function StackHeader({title}: {title: string}) { export default function StackHeader({title}: {title: string}) {
const navigation = useNavigation(); const navigation = useNavigation()
const dark = useDark(); const dark = useDark()
return ( return (
<Appbar.Header> <Appbar.Header>
@ -21,16 +21,16 @@ export default function StackHeader({title}: {title: string}) {
color={dark ? 'white' : 'white'} color={dark ? 'white' : 'white'}
onPress={() => onPress={() =>
captureScreen().then(async uri => { captureScreen().then(async uri => {
const base64 = await FileSystem.readFile(uri, 'base64'); const base64 = await FileSystem.readFile(uri, 'base64')
const url = `data:image/jpeg;base64,${base64}`; const url = `data:image/jpeg;base64,${base64}`
Share.open({ Share.open({
type: 'image/jpeg', type: 'image/jpeg',
url, url,
}); })
}) })
} }
icon="share" icon="share"
/> />
</Appbar.Header> </Appbar.Header>
); )
} }

View File

@ -1,50 +1,50 @@
import {RouteProp, useFocusEffect, useRoute} from '@react-navigation/native'; import {RouteProp, useFocusEffect, useRoute} from '@react-navigation/native'
import {useCallback, useMemo, useRef, useState} from 'react'; import {useCallback, useMemo, useRef, useState} from 'react'
import {NativeModules, TextInput, View} from 'react-native'; import {NativeModules, TextInput, View} from 'react-native'
import {FlatList} from 'react-native-gesture-handler'; import {FlatList} from 'react-native-gesture-handler'
import {Button} from 'react-native-paper'; import {Button} from 'react-native-paper'
import {getBestSet} from './best.service'; import {getBestSet} from './best.service'
import {PADDING} from './constants'; import {PADDING} from './constants'
import CountMany from './count-many'; import CountMany from './count-many'
import {AppDataSource} from './data-source'; import {AppDataSource} from './data-source'
import {getNow, setRepo} from './db'; import {getNow, setRepo} from './db'
import GymSet from './gym-set'; import GymSet from './gym-set'
import MassiveInput from './MassiveInput'; import MassiveInput from './MassiveInput'
import {useSnackbar} from './MassiveSnack'; import {useSnackbar} from './MassiveSnack'
import {PlanPageParams} from './plan-page-params'; import {PlanPageParams} from './plan-page-params'
import SetForm from './SetForm'; import SetForm from './SetForm'
import StackHeader from './StackHeader'; import StackHeader from './StackHeader'
import StartPlanItem from './StartPlanItem'; import StartPlanItem from './StartPlanItem'
import {useSettings} from './use-settings'; import {useSettings} from './use-settings'
export default function StartPlan() { export default function StartPlan() {
const {params} = useRoute<RouteProp<PlanPageParams, 'StartPlan'>>(); const {params} = useRoute<RouteProp<PlanPageParams, 'StartPlan'>>()
const [name, setName] = useState(''); const [name, setName] = useState('')
const [reps, setReps] = useState(''); const [reps, setReps] = useState('')
const [weight, setWeight] = useState(''); const [weight, setWeight] = useState('')
const [unit, setUnit] = useState<string>('kg'); const [unit, setUnit] = useState<string>('kg')
const {toast} = useSnackbar(); const {toast} = useSnackbar()
const [minutes, setMinutes] = useState(3); const [minutes, setMinutes] = useState(3)
const [seconds, setSeconds] = useState(30); const [seconds, setSeconds] = useState(30)
const [best, setBest] = useState<GymSet>(); const [best, setBest] = useState<GymSet>()
const [selected, setSelected] = useState(0); const [selected, setSelected] = useState(0)
const {settings} = useSettings(); const {settings} = useSettings()
const [counts, setCounts] = useState<CountMany[]>(); const [counts, setCounts] = useState<CountMany[]>()
const weightRef = useRef<TextInput>(null); const weightRef = useRef<TextInput>(null)
const repsRef = useRef<TextInput>(null); const repsRef = useRef<TextInput>(null)
const unitRef = useRef<TextInput>(null); const unitRef = useRef<TextInput>(null)
const workouts = useMemo(() => params.plan.workouts.split(','), [params]); const workouts = useMemo(() => params.plan.workouts.split(','), [params])
const [selection, setSelection] = useState({ const [selection, setSelection] = useState({
start: 0, start: 0,
end: 0, end: 0,
}); })
const refresh = useCallback(() => { const refresh = useCallback(() => {
const questions = workouts const questions = workouts
.map((workout, index) => `('${workout}',${index})`) .map((workout, index) => `('${workout}',${index})`)
.join(','); .join(',')
console.log({questions, workouts}); console.log({questions, workouts})
const select = ` const select = `
SELECT workouts.name, COUNT(sets.id) as total SELECT workouts.name, COUNT(sets.id) as total
FROM (select 0 as name, 0 as sequence union values ${questions}) as workouts FROM (select 0 as name, 0 as sequence union values ${questions}) as workouts
@ -55,43 +55,43 @@ export default function StartPlan() {
ORDER BY workouts.sequence ORDER BY workouts.sequence
LIMIT -1 LIMIT -1
OFFSET 1 OFFSET 1
`; `
return AppDataSource.manager.query(select).then(newCounts => { return AppDataSource.manager.query(select).then(newCounts => {
setCounts(newCounts); setCounts(newCounts)
console.log(`${StartPlan.name}.focus:`, {newCounts}); console.log(`${StartPlan.name}.focus:`, {newCounts})
return newCounts; return newCounts
}); })
}, [workouts]); }, [workouts])
const select = useCallback( const select = useCallback(
async (index: number, newCounts?: CountMany[]) => { async (index: number, newCounts?: CountMany[]) => {
setSelected(index); setSelected(index)
console.log(`${StartPlan.name}.next:`, {name, index}); console.log(`${StartPlan.name}.next:`, {name, index})
if (!counts && !newCounts) return; if (!counts && !newCounts) return
const workout = counts ? counts[index] : newCounts[index]; const workout = counts ? counts[index] : newCounts[index]
console.log(`${StartPlan.name}.next:`, {workout}); console.log(`${StartPlan.name}.next:`, {workout})
const newBest = await getBestSet(workout.name); const newBest = await getBestSet(workout.name)
console.log(`${StartPlan.name}.next:`, {newBest}); console.log(`${StartPlan.name}.next:`, {newBest})
setMinutes(newBest.minutes); setMinutes(newBest.minutes)
setSeconds(newBest.seconds); setSeconds(newBest.seconds)
setName(newBest.name); setName(newBest.name)
setReps(newBest.reps.toString()); setReps(newBest.reps.toString())
setWeight(newBest.weight.toString()); setWeight(newBest.weight.toString())
setUnit(newBest.unit); setUnit(newBest.unit)
setBest(newBest); setBest(newBest)
}, },
[name, counts], [name, counts],
); )
useFocusEffect( useFocusEffect(
useCallback(() => { useCallback(() => {
refresh().then(newCounts => select(0, newCounts)); refresh().then(newCounts => select(0, newCounts))
}, [refresh]), }, [refresh]),
); )
const handleSubmit = async () => { const handleSubmit = async () => {
console.log(`${SetForm.name}.handleSubmit:`, {reps, weight, unit, best}); console.log(`${SetForm.name}.handleSubmit:`, {reps, weight, unit, best})
const [{now}] = await getNow(); const [{now}] = await getNow()
await setRepo.save({ await setRepo.save({
name, name,
weight: +weight, weight: +weight,
@ -102,30 +102,30 @@ export default function StartPlan() {
seconds, seconds,
sets: best.sets, sets: best.sets,
hidden: false, hidden: false,
}); })
await refresh(); await refresh()
if ( if (
settings.notify && settings.notify &&
(+weight > best.weight || (+reps > best.reps && +weight === best.weight)) (+weight > best.weight || (+reps > best.reps && +weight === best.weight))
) )
toast("Great work King! That's a new record.", 5000); toast("Great work King! That's a new record.", 5000)
else if (settings.alarm) toast('Resting...', 3000); else if (settings.alarm) toast('Resting...', 3000)
else toast('Added set', 3000); else toast('Added set', 3000)
if (!settings.alarm) return; if (!settings.alarm) return
const milliseconds = Number(minutes) * 60 * 1000 + Number(seconds) * 1000; const milliseconds = Number(minutes) * 60 * 1000 + Number(seconds) * 1000
const {vibrate, sound, noSound} = settings; const {vibrate, sound, noSound} = settings
const args = [milliseconds, !!vibrate, sound, !!noSound]; const args = [milliseconds, !!vibrate, sound, !!noSound]
NativeModules.AlarmModule.timer(...args); NativeModules.AlarmModule.timer(...args)
}; }
const handleUnit = useCallback( const handleUnit = useCallback(
(value: string) => { (value: string) => {
setUnit(value.replace(/,|'/g, '')); setUnit(value.replace(/,|'/g, ''))
if (value.match(/,|'/)) if (value.match(/,|'/))
toast('Commas and single quotes would break CSV exports', 6000); toast('Commas and single quotes would break CSV exports', 6000)
}, },
[toast], [toast],
); )
return ( return (
<> <>
@ -179,5 +179,5 @@ export default function StartPlan() {
</Button> </Button>
</View> </View>
</> </>
); )
} }

View File

@ -1,40 +1,40 @@
import React, {useCallback, useState} from 'react'; import React, {useCallback, useState} from 'react'
import {GestureResponderEvent, ListRenderItemInfo, View} from 'react-native'; import {GestureResponderEvent, ListRenderItemInfo, View} from 'react-native'
import {List, Menu, RadioButton} from 'react-native-paper'; import {List, Menu, RadioButton} from 'react-native-paper'
import {useColor} from './color'; import {useColor} from './color'
import CountMany from './count-many'; import CountMany from './count-many'
import {setRepo} from './db'; import {setRepo} from './db'
interface Props extends ListRenderItemInfo<CountMany> { interface Props extends ListRenderItemInfo<CountMany> {
onSelect: (index: number) => void; onSelect: (index: number) => void
selected: number; selected: number
onUndo: () => void; onUndo: () => void
} }
export default function StartPlanItem(props: Props) { export default function StartPlanItem(props: Props) {
const {index, item, onSelect, selected, onUndo} = props; const {index, item, onSelect, selected, onUndo} = props
const {color} = useColor(); const {color} = useColor()
const [anchor, setAnchor] = useState({x: 0, y: 0}); const [anchor, setAnchor] = useState({x: 0, y: 0})
const [showMenu, setShowMenu] = useState(false); const [showMenu, setShowMenu] = useState(false)
const undo = useCallback(async () => { const undo = useCallback(async () => {
const first = await setRepo.findOne({ const first = await setRepo.findOne({
where: {name: item.name, hidden: 0 as any}, where: {name: item.name, hidden: 0 as any},
order: {created: 'desc'}, order: {created: 'desc'},
}); })
console.log({first}); console.log({first})
await setRepo.delete(first.id); await setRepo.delete(first.id)
setShowMenu(false); setShowMenu(false)
onUndo(); onUndo()
}, [setShowMenu, onUndo, item.name]); }, [setShowMenu, onUndo, item.name])
const longPress = useCallback( const longPress = useCallback(
(e: GestureResponderEvent) => { (e: GestureResponderEvent) => {
setAnchor({x: e.nativeEvent.pageX, y: e.nativeEvent.pageY}); setAnchor({x: e.nativeEvent.pageX, y: e.nativeEvent.pageY})
setShowMenu(true); setShowMenu(true)
}, },
[setShowMenu, setAnchor], [setShowMenu, setAnchor],
); )
return ( return (
<List.Item <List.Item
@ -63,5 +63,5 @@ export default function StartPlanItem(props: Props) {
</> </>
)} )}
/> />
); )
} }

View File

@ -1,11 +1,11 @@
import {useMemo} from 'react'; import {useMemo} from 'react'
import {Pressable} from 'react-native'; import {Pressable} from 'react-native'
import {Switch as PaperSwitch, Text} from 'react-native-paper'; import {Switch as PaperSwitch, Text} from 'react-native-paper'
import {CombinedDarkTheme, CombinedDefaultTheme} from './App'; import {CombinedDarkTheme, CombinedDefaultTheme} from './App'
import {useColor} from './color'; import {useColor} from './color'
import {colorShade} from './colors'; import {colorShade} from './colors'
import {MARGIN} from './constants'; import {MARGIN} from './constants'
import useDark from './use-dark'; import useDark from './use-dark'
export default function Switch({ export default function Switch({
value, value,
@ -13,25 +13,25 @@ export default function Switch({
onPress, onPress,
children, children,
}: { }: {
value?: boolean; value?: boolean
onValueChange: (value: boolean) => void; onValueChange: (value: boolean) => void
onPress: () => void; onPress: () => void
children: string; children: string
}) { }) {
const {color} = useColor(); const {color} = useColor()
const dark = useDark(); const dark = useDark()
const track = useMemo(() => { const track = useMemo(() => {
if (dark) if (dark)
return { return {
false: CombinedDarkTheme.colors.placeholder, false: CombinedDarkTheme.colors.placeholder,
true: colorShade(color, -40), true: colorShade(color, -40),
}; }
return { return {
false: CombinedDefaultTheme.colors.placeholder, false: CombinedDefaultTheme.colors.placeholder,
true: colorShade(color, -40), true: colorShade(color, -40),
}; }
}, [dark, color]); }, [dark, color])
return ( return (
<Pressable <Pressable
@ -50,5 +50,5 @@ export default function Switch({
/> />
<Text>{children}</Text> <Text>{children}</Text>
</Pressable> </Pressable>
); )
} }

View File

@ -1,35 +1,35 @@
import {Picker} from '@react-native-picker/picker'; import {Picker} from '@react-native-picker/picker'
import {RouteProp, useRoute} from '@react-navigation/native'; import {RouteProp, useRoute} from '@react-navigation/native'
import {useEffect, useState} from 'react'; import {useEffect, useState} from 'react'
import {View} from 'react-native'; import {View} from 'react-native'
import {BestPageParams} from './BestPage'; import {BestPageParams} from './BestPage'
import Chart from './Chart'; import Chart from './Chart'
import {PADDING} from './constants'; import {PADDING} from './constants'
import {setRepo} from './db'; import {setRepo} from './db'
import GymSet from './gym-set'; import GymSet from './gym-set'
import {Metrics} from './metrics'; import {Metrics} from './metrics'
import {Periods} from './periods'; import {Periods} from './periods'
import StackHeader from './StackHeader'; import StackHeader from './StackHeader'
import {formatMonth} from './time'; import {formatMonth} from './time'
import useDark from './use-dark'; import useDark from './use-dark'
import Volume from './volume'; import Volume from './volume'
export default function ViewBest() { export default function ViewBest() {
const {params} = useRoute<RouteProp<BestPageParams, 'ViewBest'>>(); const {params} = useRoute<RouteProp<BestPageParams, 'ViewBest'>>()
const dark = useDark(); const dark = useDark()
const [weights, setWeights] = useState<GymSet[]>([]); const [weights, setWeights] = useState<GymSet[]>([])
const [volumes, setVolumes] = useState<Volume[]>([]); const [volumes, setVolumes] = useState<Volume[]>([])
const [metric, setMetric] = useState(Metrics.Weight); const [metric, setMetric] = useState(Metrics.Weight)
const [period, setPeriod] = useState(Periods.Monthly); const [period, setPeriod] = useState(Periods.Monthly)
useEffect(() => { useEffect(() => {
console.log(`${ViewBest.name}.useEffect`, {metric}); console.log(`${ViewBest.name}.useEffect`, {metric})
console.log(`${ViewBest.name}.useEffect`, {period}); console.log(`${ViewBest.name}.useEffect`, {period})
let difference = '-7 days'; let difference = '-7 days'
if (period === Periods.Monthly) difference = '-1 months'; if (period === Periods.Monthly) difference = '-1 months'
else if (period === Periods.Yearly) difference = '-1 years'; else if (period === Periods.Yearly) difference = '-1 years'
let group = '%Y-%m-%d'; let group = '%Y-%m-%d'
if (period === Periods.Yearly) group = '%Y-%m'; if (period === Periods.Yearly) group = '%Y-%m'
const builder = setRepo const builder = setRepo
.createQueryBuilder() .createQueryBuilder()
.select("STRFTIME('%Y-%m-%d', created)", 'created') .select("STRFTIME('%Y-%m-%d', created)", 'created')
@ -40,31 +40,28 @@ export default function ViewBest() {
difference, difference,
}) })
.groupBy('name') .groupBy('name')
.addGroupBy(`STRFTIME('${group}', created)`); .addGroupBy(`STRFTIME('${group}', created)`)
switch (metric) { switch (metric) {
case Metrics.Weight: case Metrics.Weight:
builder builder.addSelect('MAX(weight)', 'weight').getRawMany().then(setWeights)
.addSelect('MAX(weight)', 'weight') break
.getRawMany()
.then(setWeights);
break;
case Metrics.Volume: case Metrics.Volume:
builder builder
.addSelect('SUM(weight * reps)', 'value') .addSelect('SUM(weight * reps)', 'value')
.getRawMany() .getRawMany()
.then(setVolumes); .then(setVolumes)
break; break
default: default:
// Brzycki formula https://en.wikipedia.org/wiki/One-repetition_maximum#Brzycki // Brzycki formula https://en.wikipedia.org/wiki/One-repetition_maximum#Brzycki
builder builder
.addSelect('MAX(weight / (1.0278 - 0.0278 * reps))', 'weight') .addSelect('MAX(weight / (1.0278 - 0.0278 * reps))', 'weight')
.getRawMany() .getRawMany()
.then(weights => { .then(weights => {
console.log({weights}); console.log({weights})
setWeights(weights); setWeights(weights)
}); })
} }
}, [params.best.name, metric, period]); }, [params.best.name, metric, period])
return ( return (
<> <>
@ -109,5 +106,5 @@ export default function ViewBest() {
)} )}
</View> </View>
</> </>
); )
} }

View File

@ -1,48 +1,48 @@
import {NavigationProp, useNavigation} from '@react-navigation/native'; import {NavigationProp, useNavigation} from '@react-navigation/native'
import {useCallback, useMemo, useState} from 'react'; import {useCallback, useMemo, useState} from 'react'
import {GestureResponderEvent, Image} from 'react-native'; import {GestureResponderEvent, Image} from 'react-native'
import {List, Menu, Text} from 'react-native-paper'; import {List, Menu, Text} from 'react-native-paper'
import ConfirmDialog from './ConfirmDialog'; import ConfirmDialog from './ConfirmDialog'
import {setRepo} from './db'; import {setRepo} from './db'
import GymSet from './gym-set'; import GymSet from './gym-set'
import {useSettings} from './use-settings'; import {useSettings} from './use-settings'
import {WorkoutsPageParams} from './WorkoutsPage'; import {WorkoutsPageParams} from './WorkoutsPage'
export default function WorkoutItem({ export default function WorkoutItem({
item, item,
onRemove, onRemove,
}: { }: {
item: GymSet; item: GymSet
onRemove: () => void; onRemove: () => void
}) { }) {
const [showMenu, setShowMenu] = useState(false); const [showMenu, setShowMenu] = useState(false)
const [anchor, setAnchor] = useState({x: 0, y: 0}); const [anchor, setAnchor] = useState({x: 0, y: 0})
const [showRemove, setShowRemove] = useState(''); const [showRemove, setShowRemove] = useState('')
const {settings} = useSettings(); const {settings} = useSettings()
const navigation = useNavigation<NavigationProp<WorkoutsPageParams>>(); const navigation = useNavigation<NavigationProp<WorkoutsPageParams>>()
const remove = useCallback(async () => { const remove = useCallback(async () => {
await setRepo.delete({name: item.name}); await setRepo.delete({name: item.name})
setShowMenu(false); setShowMenu(false)
onRemove(); onRemove()
}, [setShowMenu, onRemove, item.name]); }, [setShowMenu, onRemove, item.name])
const longPress = useCallback( const longPress = useCallback(
(e: GestureResponderEvent) => { (e: GestureResponderEvent) => {
setAnchor({x: e.nativeEvent.pageX, y: e.nativeEvent.pageY}); setAnchor({x: e.nativeEvent.pageX, y: e.nativeEvent.pageY})
setShowMenu(true); setShowMenu(true)
}, },
[setShowMenu, setAnchor], [setShowMenu, setAnchor],
); )
const description = useMemo(() => { const description = useMemo(() => {
const seconds = item.seconds?.toString().padStart(2, '0'); const seconds = item.seconds?.toString().padStart(2, '0')
if (settings.alarm && settings.showSets) if (settings.alarm && settings.showSets)
return `${item.sets} x ${item.minutes || 0}:${seconds}`; return `${item.sets} x ${item.minutes || 0}:${seconds}`
else if (settings.alarm && !settings.showSets) else if (settings.alarm && !settings.showSets)
return `${item.minutes || 0}:${seconds}`; return `${item.minutes || 0}:${seconds}`
return `${item.sets}`; return `${item.sets}`
}, [item, settings]); }, [item, settings])
return ( return (
<> <>
@ -69,8 +69,8 @@ export default function WorkoutItem({
<Menu.Item <Menu.Item
icon="delete" icon="delete"
onPress={() => { onPress={() => {
setShowRemove(item.name); setShowRemove(item.name)
setShowMenu(false); setShowMenu(false)
}} }}
title="Delete" title="Delete"
/> />
@ -87,5 +87,5 @@ export default function WorkoutItem({
sure? sure?
</ConfirmDialog> </ConfirmDialog>
</> </>
); )
} }

View File

@ -2,26 +2,26 @@ import {
NavigationProp, NavigationProp,
useFocusEffect, useFocusEffect,
useNavigation, useNavigation,
} from '@react-navigation/native'; } from '@react-navigation/native'
import {useCallback, useState} from 'react'; import {useCallback, useState} from 'react'
import {FlatList} from 'react-native'; import {FlatList} from 'react-native'
import {List} from 'react-native-paper'; import {List} from 'react-native-paper'
import DrawerHeader from './DrawerHeader'; import DrawerHeader from './DrawerHeader'
import Page from './Page'; import Page from './Page'
import GymSet from './gym-set'; import GymSet from './gym-set'
import SetList from './SetList'; import SetList from './SetList'
import WorkoutItem from './WorkoutItem'; import WorkoutItem from './WorkoutItem'
import {WorkoutsPageParams} from './WorkoutsPage'; import {WorkoutsPageParams} from './WorkoutsPage'
import {setRepo} from './db'; import {setRepo} from './db'
const limit = 15; const limit = 15
export default function WorkoutList() { export default function WorkoutList() {
const [workouts, setWorkouts] = useState<GymSet[]>(); const [workouts, setWorkouts] = useState<GymSet[]>()
const [offset, setOffset] = useState(0); const [offset, setOffset] = useState(0)
const [term, setTerm] = useState(''); const [term, setTerm] = useState('')
const [end, setEnd] = useState(false); const [end, setEnd] = useState(false)
const navigation = useNavigation<NavigationProp<WorkoutsPageParams>>(); const navigation = useNavigation<NavigationProp<WorkoutsPageParams>>()
const refresh = useCallback(async (value: string) => { const refresh = useCallback(async (value: string) => {
const newWorkouts = await setRepo const newWorkouts = await setRepo
@ -31,35 +31,35 @@ export default function WorkoutList() {
.groupBy('name') .groupBy('name')
.orderBy('name') .orderBy('name')
.limit(limit) .limit(limit)
.getMany(); .getMany()
console.log(`${WorkoutList.name}`, {newWorkout: newWorkouts[0]}); console.log(`${WorkoutList.name}`, {newWorkout: newWorkouts[0]})
setWorkouts(newWorkouts); setWorkouts(newWorkouts)
setOffset(0); setOffset(0)
setEnd(false); setEnd(false)
}, []); }, [])
useFocusEffect( useFocusEffect(
useCallback(() => { useCallback(() => {
refresh(term); refresh(term)
}, [refresh, term]), }, [refresh, term]),
); )
const renderItem = useCallback( const renderItem = useCallback(
({item}: {item: GymSet}) => ( ({item}: {item: GymSet}) => (
<WorkoutItem item={item} key={item.name} onRemove={() => refresh(term)} /> <WorkoutItem item={item} key={item.name} onRemove={() => refresh(term)} />
), ),
[refresh, term], [refresh, term],
); )
const next = useCallback(async () => { const next = useCallback(async () => {
if (end) return; if (end) return
const newOffset = offset + limit; const newOffset = offset + limit
console.log(`${SetList.name}.next:`, { console.log(`${SetList.name}.next:`, {
offset, offset,
limit, limit,
newOffset, newOffset,
term, term,
}); })
const newWorkouts = await setRepo const newWorkouts = await setRepo
.createQueryBuilder() .createQueryBuilder()
.select() .select()
@ -68,27 +68,27 @@ export default function WorkoutList() {
.orderBy('name') .orderBy('name')
.limit(limit) .limit(limit)
.offset(newOffset) .offset(newOffset)
.getMany(); .getMany()
if (newWorkouts.length === 0) return setEnd(true); if (newWorkouts.length === 0) return setEnd(true)
if (!workouts) return; if (!workouts) return
setWorkouts([...workouts, ...newWorkouts]); setWorkouts([...workouts, ...newWorkouts])
if (newWorkouts.length < limit) return setEnd(true); if (newWorkouts.length < limit) return setEnd(true)
setOffset(newOffset); setOffset(newOffset)
}, [term, end, offset, workouts]); }, [term, end, offset, workouts])
const onAdd = useCallback(async () => { const onAdd = useCallback(async () => {
navigation.navigate('EditWorkout', { navigation.navigate('EditWorkout', {
value: new GymSet(), value: new GymSet(),
}); })
}, [navigation]); }, [navigation])
const search = useCallback( const search = useCallback(
(value: string) => { (value: string) => {
setTerm(value); setTerm(value)
refresh(value); refresh(value)
}, },
[refresh], [refresh],
); )
return ( return (
<> <>
@ -110,5 +110,5 @@ export default function WorkoutList() {
)} )}
</Page> </Page>
</> </>
); )
} }

View File

@ -1,16 +1,16 @@
import {createStackNavigator} from '@react-navigation/stack'; import {createStackNavigator} from '@react-navigation/stack'
import EditWorkout from './EditWorkout'; import EditWorkout from './EditWorkout'
import GymSet from './gym-set'; import GymSet from './gym-set'
import WorkoutList from './WorkoutList'; import WorkoutList from './WorkoutList'
export type WorkoutsPageParams = { export type WorkoutsPageParams = {
WorkoutList: {}; WorkoutList: {}
EditWorkout: { EditWorkout: {
value: GymSet; value: GymSet
}; }
}; }
const Stack = createStackNavigator<WorkoutsPageParams>(); const Stack = createStackNavigator<WorkoutsPageParams>()
export default function WorkoutsPage() { export default function WorkoutsPage() {
return ( return (
@ -19,5 +19,5 @@ export default function WorkoutsPage() {
<Stack.Screen name="WorkoutList" component={WorkoutList} /> <Stack.Screen name="WorkoutList" component={WorkoutList} />
<Stack.Screen name="EditWorkout" component={EditWorkout} /> <Stack.Screen name="EditWorkout" component={EditWorkout} />
</Stack.Navigator> </Stack.Navigator>
); )
} }

View File

@ -1,5 +1,5 @@
import {setRepo} from './db'; import {setRepo} from './db'
import GymSet from './gym-set'; import GymSet from './gym-set'
export const getBestSet = async (name: string): Promise<GymSet> => { export const getBestSet = async (name: string): Promise<GymSet> => {
return setRepo return setRepo
@ -11,5 +11,5 @@ export const getBestSet = async (name: string): Promise<GymSet> => {
.addGroupBy('reps') .addGroupBy('reps')
.orderBy('weight', 'DESC') .orderBy('weight', 'DESC')
.addOrderBy('reps', 'DESC') .addOrderBy('reps', 'DESC')
.getOne(); .getOne()
}; }

View File

@ -1,11 +1,11 @@
import React, {useContext} from 'react'; import React, {useContext} from 'react'
export const Color = React.createContext({ export const Color = React.createContext({
color: '', color: '',
setColor: (_value: string) => {}, setColor: (_value: string) => {},
}); })
export const useColor = () => { export const useColor = () => {
const context = useContext(Color); const context = useContext(Color)
return context; return context
}; }

View File

@ -3,34 +3,34 @@ export const lightColors = [
{hex: '#B3E5FC', name: 'Cyan'}, {hex: '#B3E5FC', name: 'Cyan'},
{hex: '#FFC0CB', name: 'Pink'}, {hex: '#FFC0CB', name: 'Pink'},
{hex: '#E9DCC9', name: 'Linen'}, {hex: '#E9DCC9', name: 'Linen'},
]; ]
export const darkColors = [ export const darkColors = [
{hex: '#8156A7', name: 'Purple'}, {hex: '#8156A7', name: 'Purple'},
{hex: '#007AFF', name: 'Blue'}, {hex: '#007AFF', name: 'Blue'},
{hex: '#000000', name: 'Black'}, {hex: '#000000', name: 'Black'},
{hex: '#CD5C5C', name: 'Red'}, {hex: '#CD5C5C', name: 'Red'},
]; ]
export const colorShade = (color: any, amount: number) => { export const colorShade = (color: any, amount: number) => {
color = color.replace(/^#/, ''); color = color.replace(/^#/, '')
if (color.length === 3) if (color.length === 3)
color = color[0] + color[0] + color[1] + color[1] + color[2] + color[2]; color = color[0] + color[0] + color[1] + color[1] + color[2] + color[2]
let [r, g, b] = color.match(/.{2}/g); let [r, g, b] = color.match(/.{2}/g)
[r, g, b] = [ ;[r, g, b] = [
parseInt(r, 16) + amount, parseInt(r, 16) + amount,
parseInt(g, 16) + amount, parseInt(g, 16) + amount,
parseInt(b, 16) + amount, parseInt(b, 16) + amount,
]; ]
r = Math.max(Math.min(255, r), 0).toString(16); r = Math.max(Math.min(255, r), 0).toString(16)
g = Math.max(Math.min(255, g), 0).toString(16); g = Math.max(Math.min(255, g), 0).toString(16)
b = Math.max(Math.min(255, b), 0).toString(16); b = Math.max(Math.min(255, b), 0).toString(16)
const rr = (r.length < 2 ? '0' : '') + r; const rr = (r.length < 2 ? '0' : '') + r
const gg = (g.length < 2 ? '0' : '') + g; const gg = (g.length < 2 ? '0' : '') + g
const bb = (b.length < 2 ? '0' : '') + b; const bb = (b.length < 2 ? '0' : '') + b
return `#${rr}${gg}${bb}`; return `#${rr}${gg}${bb}`
}; }

View File

@ -1,2 +1,2 @@
export const MARGIN = 10; export const MARGIN = 10
export const PADDING = 10; export const PADDING = 10

View File

@ -1,5 +1,5 @@
export default interface CountMany { export default interface CountMany {
name: string; name: string
total: number; total: number
sets?: number; sets?: number
} }

View File

@ -1,29 +1,29 @@
import {DataSource} from 'typeorm'; import {DataSource} from 'typeorm'
import GymSet from './gym-set'; import GymSet from './gym-set'
import {Sets1667185586014} from './migrations/1667185586014-sets'; import {Sets1667185586014} from './migrations/1667185586014-sets'
import {plans1667186124792} from './migrations/1667186124792-plans'; import {plans1667186124792} from './migrations/1667186124792-plans'
import {settings1667186130041} from './migrations/1667186130041-settings'; import {settings1667186130041} from './migrations/1667186130041-settings'
import {addSound1667186139844} from './migrations/1667186139844-add-sound'; import {addSound1667186139844} from './migrations/1667186139844-add-sound'
import {addHidden1667186159379} from './migrations/1667186159379-add-hidden'; import {addHidden1667186159379} from './migrations/1667186159379-add-hidden'
import {addNotify1667186166140} from './migrations/1667186166140-add-notify'; import {addNotify1667186166140} from './migrations/1667186166140-add-notify'
import {addImage1667186171548} from './migrations/1667186171548-add-image'; import {addImage1667186171548} from './migrations/1667186171548-add-image'
import {addImages1667186179488} from './migrations/1667186179488-add-images'; import {addImages1667186179488} from './migrations/1667186179488-add-images'
import {insertSettings1667186203827} from './migrations/1667186203827-insert-settings'; import {insertSettings1667186203827} from './migrations/1667186203827-insert-settings'
import {addSteps1667186211251} from './migrations/1667186211251-add-steps'; import {addSteps1667186211251} from './migrations/1667186211251-add-steps'
import {addSets1667186250618} from './migrations/1667186250618-add-sets'; import {addSets1667186250618} from './migrations/1667186250618-add-sets'
import {addMinutes1667186255650} from './migrations/1667186255650-add-minutes'; import {addMinutes1667186255650} from './migrations/1667186255650-add-minutes'
import {addSeconds1667186259174} from './migrations/1667186259174-add-seconds'; import {addSeconds1667186259174} from './migrations/1667186259174-add-seconds'
import {addShowUnit1667186265588} from './migrations/1667186265588-add-show-unit'; import {addShowUnit1667186265588} from './migrations/1667186265588-add-show-unit'
import {addColor1667186320954} from './migrations/1667186320954-add-color'; import {addColor1667186320954} from './migrations/1667186320954-add-color'
import {addSteps1667186348425} from './migrations/1667186348425-add-steps'; import {addSteps1667186348425} from './migrations/1667186348425-add-steps'
import {addDate1667186431804} from './migrations/1667186431804-add-date'; import {addDate1667186431804} from './migrations/1667186431804-add-date'
import {addShowDate1667186435051} from './migrations/1667186435051-add-show-date'; import {addShowDate1667186435051} from './migrations/1667186435051-add-show-date'
import {addTheme1667186439366} from './migrations/1667186439366-add-theme'; import {addTheme1667186439366} from './migrations/1667186439366-add-theme'
import {addShowSets1667186443614} from './migrations/1667186443614-add-show-sets'; import {addShowSets1667186443614} from './migrations/1667186443614-add-show-sets'
import {addSetsCreated1667186451005} from './migrations/1667186451005-add-sets-created'; import {addSetsCreated1667186451005} from './migrations/1667186451005-add-sets-created'
import {addNoSound1667186456118} from './migrations/1667186456118-add-no-sound'; import {addNoSound1667186456118} from './migrations/1667186456118-add-no-sound'
import {Plan} from './plan'; import {Plan} from './plan'
import Settings from './settings'; import Settings from './settings'
export const AppDataSource = new DataSource({ export const AppDataSource = new DataSource({
type: 'react-native', type: 'react-native',
@ -55,4 +55,4 @@ export const AppDataSource = new DataSource({
addSetsCreated1667186451005, addSetsCreated1667186451005,
addNoSound1667186456118, addNoSound1667186456118,
], ],
}); })

34
db.ts
View File

@ -1,26 +1,26 @@
import {enablePromise, SQLiteDatabase} from 'react-native-sqlite-storage'; import {enablePromise, SQLiteDatabase} from 'react-native-sqlite-storage'
import {AppDataSource} from './data-source'; import {AppDataSource} from './data-source'
import GymSet from './gym-set'; import GymSet from './gym-set'
import {Plan} from './plan'; import {Plan} from './plan'
import Settings from './settings'; import Settings from './settings'
enablePromise(true); enablePromise(true)
export let db: SQLiteDatabase; export let db: SQLiteDatabase
export const setRepo = AppDataSource.manager.getRepository(GymSet); export const setRepo = AppDataSource.manager.getRepository(GymSet)
export const planRepo = AppDataSource.manager.getRepository(Plan); export const planRepo = AppDataSource.manager.getRepository(Plan)
export const settingsRepo = AppDataSource.manager.getRepository(Settings); export const settingsRepo = AppDataSource.manager.getRepository(Settings)
export const getNow = (): Promise<{now: string}[]> => { export const getNow = (): Promise<{now: string}[]> => {
return AppDataSource.manager.query( return AppDataSource.manager.query(
"SELECT STRFTIME('%Y-%m-%dT%H:%M:%S','now','localtime') AS now", "SELECT STRFTIME('%Y-%m-%dT%H:%M:%S','now','localtime') AS now",
); )
}; }
export const runMigrations = async () => { export const runMigrations = async () => {
console.log(`${runMigrations.name}:`, 'Initializing...'); console.log(`${runMigrations.name}:`, 'Initializing...')
await AppDataSource.initialize(); await AppDataSource.initialize()
console.log(`${runMigrations.name}:`, 'Running migrations...'); console.log(`${runMigrations.name}:`, 'Running migrations...')
await AppDataSource.runMigrations(); await AppDataSource.runMigrations()
}; }

View File

@ -1,7 +1,7 @@
export type DrawerParamList = { export type DrawerParamList = {
Home: {}; Home: {}
Settings: {}; Settings: {}
Best: {}; Best: {}
Plans: {}; Plans: {}
Workouts: {}; Workouts: {}
}; }

View File

@ -1,40 +1,40 @@
import {Column, Entity, PrimaryGeneratedColumn} from 'typeorm'; import {Column, Entity, PrimaryGeneratedColumn} from 'typeorm'
@Entity('sets') @Entity('sets')
export default class GymSet { export default class GymSet {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
id?: number; id?: number
@Column('text') @Column('text')
name: string; name: string
@Column('int') @Column('int')
reps: number; reps: number
@Column('int') @Column('int')
weight: number; weight: number
@Column('int') @Column('int')
sets = 3; sets = 3
@Column('int') @Column('int')
minutes = 3; minutes = 3
@Column('int') @Column('int')
seconds = 30; seconds = 30
@Column('boolean') @Column('boolean')
hidden = false; hidden = false
@Column('text') @Column('text')
created: string; created: string
@Column('text') @Column('text')
unit: string; unit: string
@Column('text') @Column('text')
image: string; image: string
@Column('text') @Column('text')
steps?: string; steps?: string
} }

View File

@ -1,8 +1,8 @@
import GymSet from './gym-set'; import GymSet from './gym-set'
export type HomePageParams = { export type HomePageParams = {
Sets: {}; Sets: {}
EditSet: { EditSet: {
set: GymSet; set: GymSet
}; }
}; }

View File

@ -1,5 +1,5 @@
export default interface Input<T> { export default interface Input<T> {
name: string; name: string
value?: T; value?: T
onChange: (value: T) => void; onChange: (value: T) => void
} }

View File

@ -1,5 +1,5 @@
import 'react-native-gesture-handler/jestSetup'; import 'react-native-gesture-handler/jestSetup'
import {NativeModules as RNNativeModules} from 'react-native'; import {NativeModules as RNNativeModules} from 'react-native'
//RNNativeModules.UIManager = RNNativeModules.UIManager || {}; //RNNativeModules.UIManager = RNNativeModules.UIManager || {};
//RNNativeModules.UIManager.RCTView = RNNativeModules.UIManager.RCTView || {}; //RNNativeModules.UIManager.RCTView = RNNativeModules.UIManager.RCTView || {};
@ -16,14 +16,14 @@ import {NativeModules as RNNativeModules} from 'react-native';
//}; //};
RNNativeModules.RNViewShot = RNNativeModules.RNViewShot || { RNNativeModules.RNViewShot = RNNativeModules.RNViewShot || {
captureScreen: jest.fn(), captureScreen: jest.fn(),
}; }
jest.mock('react-native-file-access', () => jest.fn()); jest.mock('react-native-file-access', () => jest.fn())
jest.mock('react-native-share', () => jest.fn()); jest.mock('react-native-share', () => jest.fn())
jest.mock('react-native/Libraries/Animated/NativeAnimatedHelper'); jest.mock('react-native/Libraries/Animated/NativeAnimatedHelper')
jest.useFakeTimers(); jest.useFakeTimers()
jest.mock('react-native-reanimated', () => { jest.mock('react-native-reanimated', () => {
const Reanimated = require('react-native-reanimated/mock'); const Reanimated = require('react-native-reanimated/mock')
Reanimated.default.call = () => {}; Reanimated.default.call = () => {}
return Reanimated; return Reanimated
}); })

View File

@ -1,21 +1,21 @@
import {NavigationContainer} from '@react-navigation/native'; import {NavigationContainer} from '@react-navigation/native'
import React from 'react'; import React from 'react'
import {Provider as PaperProvider} from 'react-native-paper'; import {Provider as PaperProvider} from 'react-native-paper'
import {Color} from './color'; import {Color} from './color'
import {lightColors} from './colors'; import {lightColors} from './colors'
import MassiveSnack from './MassiveSnack'; import MassiveSnack from './MassiveSnack'
import {defaultSettings, SettingsContext} from './use-settings'; import {defaultSettings, SettingsContext} from './use-settings'
import MaterialIcon from 'react-native-vector-icons/MaterialIcons'; import MaterialIcon from 'react-native-vector-icons/MaterialIcons'
const color = lightColors[0].hex; const color = lightColors[0].hex
export const setColor = jest.fn(); export const setColor = jest.fn()
const settings = defaultSettings; const settings = defaultSettings
export const setSettings = jest.fn(); export const setSettings = jest.fn()
export const MockProviders = ({ export const MockProviders = ({
children, children,
}: { }: {
children: JSX.Element | JSX.Element[]; children: JSX.Element | JSX.Element[]
}) => ( }) => (
<Color.Provider value={{color, setColor}}> <Color.Provider value={{color, setColor}}>
<PaperProvider settings={{icon: props => <MaterialIcon {...props} />}}> <PaperProvider settings={{icon: props => <MaterialIcon {...props} />}}>
@ -26,4 +26,4 @@ export const MockProviders = ({
</SettingsContext.Provider> </SettingsContext.Provider>
</PaperProvider> </PaperProvider>
</Color.Provider> </Color.Provider>
); )

View File

@ -1,11 +1,11 @@
import {Plan} from './plan'; import {Plan} from './plan'
export type PlanPageParams = { export type PlanPageParams = {
PlanList: {}; PlanList: {}
EditPlan: { EditPlan: {
plan: Plan; plan: Plan
}; }
StartPlan: { StartPlan: {
plan: Plan; plan: Plan
}; }
}; }

View File

@ -1,13 +1,13 @@
import {Column, Entity, PrimaryGeneratedColumn} from 'typeorm'; import {Column, Entity, PrimaryGeneratedColumn} from 'typeorm'
@Entity('plans') @Entity('plans')
export class Plan { export class Plan {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
id?: number; id?: number
@Column('text') @Column('text')
days: string; days: string
@Column('text') @Column('text')
workouts: string; workouts: string
} }

View File

@ -1,7 +1,7 @@
import {DrawerParamList} from './drawer-param-list'; import {DrawerParamList} from './drawer-param-list'
export default interface Route { export default interface Route {
name: keyof DrawerParamList; name: keyof DrawerParamList
component: React.ComponentType<any>; component: React.ComponentType<any>
icon: string; icon: string
} }

View File

@ -1,43 +1,43 @@
import {Column, Entity, PrimaryColumn} from 'typeorm'; import {Column, Entity, PrimaryColumn} from 'typeorm'
@Entity() @Entity()
export default class Settings { export default class Settings {
@PrimaryColumn('boolean') @PrimaryColumn('boolean')
alarm: boolean; alarm: boolean
@Column('boolean') @Column('boolean')
vibrate: boolean; vibrate: boolean
@Column('text') @Column('text')
sound: string; sound: string
@Column('boolean') @Column('boolean')
notify: boolean; notify: boolean
@Column('boolean') @Column('boolean')
images: boolean; images: boolean
@Column('boolean') @Column('boolean')
showUnit: boolean; showUnit: boolean
@Column('text') @Column('text')
color: string; color: string
@Column('boolean') @Column('boolean')
steps: boolean; steps: boolean
@Column('text') @Column('text')
date: string; date: string
@Column('boolean') @Column('boolean')
showDate: boolean; showDate: boolean
@Column('text') @Column('text')
theme: string; theme: string
@Column('boolean') @Column('boolean')
showSets: boolean; showSets: boolean
@Column('boolean') @Column('boolean')
noSound: boolean; noSound: boolean
} }

50
time.ts
View File

@ -6,26 +6,26 @@ export const DAYS = [
'Thursday', 'Thursday',
'Friday', 'Friday',
'Saturday', 'Saturday',
]; ]
export function formatMonth(iso: string) { export function formatMonth(iso: string) {
const date = new Date(iso); const date = new Date(iso)
const dd = date.getDate().toString(); const dd = date.getDate().toString()
const mm = (date.getMonth() + 1).toString(); const mm = (date.getMonth() + 1).toString()
return `${dd}/${mm}`; return `${dd}/${mm}`
} }
function twelveHour(twentyFourHour: string) { function twelveHour(twentyFourHour: string) {
const [hourString, minute] = twentyFourHour.split(':'); const [hourString, minute] = twentyFourHour.split(':')
const hour = +hourString % 24; const hour = +hourString % 24
return (hour % 12 || 12) + ':' + minute + (hour < 12 ? ' AM' : ' PM'); return (hour % 12 || 12) + ':' + minute + (hour < 12 ? ' AM' : ' PM')
} }
function dayOfWeek(iso: string) { function dayOfWeek(iso: string) {
const date = new Date(iso); const date = new Date(iso)
const day = date.getDay(); const day = date.getDay()
const target = DAYS[day]; const target = DAYS[day]
return target.slice(0, 3); return target.slice(0, 3)
} }
/** /**
@ -33,29 +33,29 @@ function dayOfWeek(iso: string) {
* @param kind Intended format for the date, e.g. '%Y-%m-%d %H:%M' * @param kind Intended format for the date, e.g. '%Y-%m-%d %H:%M'
*/ */
export function format(iso: string, kind: string) { export function format(iso: string, kind: string) {
const split = iso.split('T'); const split = iso.split('T')
const [year, month, day] = split[0].split('-'); const [year, month, day] = split[0].split('-')
const time = twelveHour(split[1]); const time = twelveHour(split[1])
switch (kind) { switch (kind) {
case '%Y-%m-%d %H:%M': case '%Y-%m-%d %H:%M':
return iso.replace('T', ' ').replace(/:\d{2}/, ''); return iso.replace('T', ' ').replace(/:\d{2}/, '')
case '%Y-%m-%d': case '%Y-%m-%d':
return split[0]; return split[0]
case '%H:%M': case '%H:%M':
return split[1].replace(/:\d{2}/, ''); return split[1].replace(/:\d{2}/, '')
case '%d/%m/%y %h:%M %p': case '%d/%m/%y %h:%M %p':
return `${day}/${month}/${year} ${time}`; return `${day}/${month}/${year} ${time}`
case '%d/%m %h:%M %p': case '%d/%m %h:%M %p':
return `${day}/${month} ${time}`; return `${day}/${month} ${time}`
case '%d/%m/%y': case '%d/%m/%y':
return `${day}/${month}/${year}`; return `${day}/${month}/${year}`
case '%d/%m': case '%d/%m':
return `${day}/${month}`; return `${day}/${month}`
case '%h:%M %p': case '%h:%M %p':
return time; return time
case '%A %h:%M %p': case '%A %h:%M %p':
return dayOfWeek(iso) + ' ' + time; return dayOfWeek(iso) + ' ' + time
default: default:
return iso; return iso
} }
} }

View File

@ -1,11 +1,11 @@
import {useColorScheme} from 'react-native'; import {useColorScheme} from 'react-native'
import {useSettings} from './use-settings'; import {useSettings} from './use-settings'
export default function useDark() { export default function useDark() {
const dark = useColorScheme() === 'dark'; const dark = useColorScheme() === 'dark'
const {settings} = useSettings(); const {settings} = useSettings()
if (settings.theme === 'dark') return true; if (settings.theme === 'dark') return true
if (settings.theme === 'light') return false; if (settings.theme === 'light') return false
return dark; return dark
} }

View File

@ -1,5 +1,5 @@
import React, {useContext} from 'react'; import React, {useContext} from 'react'
import Settings from './settings'; import Settings from './settings'
export const defaultSettings: Settings = { export const defaultSettings: Settings = {
alarm: 0, alarm: 0,
@ -15,16 +15,16 @@ export const defaultSettings: Settings = {
theme: 'system', theme: 'system',
vibrate: 1, vibrate: 1,
noSound: 0, noSound: 0,
}; }
export const SettingsContext = React.createContext<{ export const SettingsContext = React.createContext<{
settings: Settings; settings: Settings
setSettings: (value: Settings) => void; setSettings: (value: Settings) => void
}>({ }>({
settings: defaultSettings, settings: defaultSettings,
setSettings: () => null, setSettings: () => null,
}); })
export function useSettings() { export function useSettings() {
return useContext(SettingsContext); return useContext(SettingsContext)
} }

View File

@ -1,6 +1,6 @@
export default interface Volume { export default interface Volume {
name: string; name: string
created: string; created: string
value: number; value: number
unit: string; unit: string
} }

View File

@ -1,18 +1,18 @@
import {NativeModules, PermissionsAndroid} from 'react-native'; import {NativeModules, PermissionsAndroid} from 'react-native'
import {Dirs, FileSystem} from 'react-native-file-access'; import {Dirs, FileSystem} from 'react-native-file-access'
export const write = async (name: string, data: string) => { export const write = async (name: string, data: string) => {
const filePath = `${Dirs.DocumentDir}/${name}`; const filePath = `${Dirs.DocumentDir}/${name}`
const permission = async () => { const permission = async () => {
const granted = await PermissionsAndroid.request( const granted = await PermissionsAndroid.request(
PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE, PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE,
); )
return granted === PermissionsAndroid.RESULTS.GRANTED; return granted === PermissionsAndroid.RESULTS.GRANTED
}; }
const granted = await permission(); const granted = await permission()
if (!granted) return; if (!granted) return
await FileSystem.writeFile(filePath, data); await FileSystem.writeFile(filePath, data)
if (!FileSystem.exists(filePath)) return; if (!FileSystem.exists(filePath)) return
await FileSystem.cpExternal(filePath, name, 'downloads'); await FileSystem.cpExternal(filePath, name, 'downloads')
NativeModules.DownloadModule.show(name); NativeModules.DownloadModule.show(name)
}; }