Merge branch 'master' into alarm-module
|
@ -5,12 +5,17 @@ module.exports = {
|
||||||
plugins: ['@typescript-eslint'],
|
plugins: ['@typescript-eslint'],
|
||||||
overrides: [
|
overrides: [
|
||||||
{
|
{
|
||||||
files: ['*.ts', '*.tsx'],
|
files: ['*.ts', '*.tsx', '*.js'],
|
||||||
rules: {
|
rules: {
|
||||||
'@typescript-eslint/no-shadow': ['error'],
|
'@typescript-eslint/no-shadow': ['error'],
|
||||||
'no-shadow': 'off',
|
'no-shadow': 'off',
|
||||||
'no-undef': 'off',
|
'no-undef': 'off',
|
||||||
|
semi: 'off',
|
||||||
|
curly: 'off',
|
||||||
|
'react/react-in-jsx-scope': 'off',
|
||||||
|
'react-native/no-inline-styles': 'off',
|
||||||
|
'no-spaced-func': 'off',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
}
|
||||||
|
|
|
@ -4,4 +4,5 @@ module.exports = {
|
||||||
bracketSpacing: false,
|
bracketSpacing: false,
|
||||||
singleQuote: true,
|
singleQuote: true,
|
||||||
trailingComma: 'all',
|
trailingComma: 'all',
|
||||||
|
semi: false,
|
||||||
};
|
};
|
||||||
|
|
133
App.tsx
|
@ -2,23 +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 React, {useEffect, useMemo, useState} from 'react';
|
import {useEffect, useMemo, useState} from 'react'
|
||||||
import {useColorScheme} from 'react-native';
|
import {DeviceEventEmitter, useColorScheme} from 'react-native'
|
||||||
|
import React from 'react'
|
||||||
import {
|
import {
|
||||||
DarkTheme as PaperDarkTheme,
|
DarkTheme as PaperDarkTheme,
|
||||||
DefaultTheme as PaperDefaultTheme,
|
DefaultTheme as PaperDefaultTheme,
|
||||||
Provider,
|
Provider as PaperProvider,
|
||||||
} from 'react-native-paper';
|
Snackbar,
|
||||||
import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
|
} from 'react-native-paper'
|
||||||
import {Color} from './color';
|
import MaterialIcon from 'react-native-vector-icons/MaterialIcons'
|
||||||
import {lightColors} from './colors';
|
import {AppDataSource} from './data-source'
|
||||||
import {runMigrations} from './db';
|
import {settingsRepo} from './db'
|
||||||
import MassiveSnack from './MassiveSnack';
|
import Routes from './Routes'
|
||||||
import Routes from './Routes';
|
import {TOAST} from './toast'
|
||||||
import Settings from './settings';
|
import {ThemeContext} from './use-theme'
|
||||||
import {getSettings} from './settings.service';
|
|
||||||
import {SettingsContext} from './use-settings';
|
|
||||||
|
|
||||||
export const CombinedDefaultTheme = {
|
export const CombinedDefaultTheme = {
|
||||||
...NavigationDefaultTheme,
|
...NavigationDefaultTheme,
|
||||||
|
@ -27,7 +26,7 @@ export const CombinedDefaultTheme = {
|
||||||
...NavigationDefaultTheme.colors,
|
...NavigationDefaultTheme.colors,
|
||||||
...PaperDefaultTheme.colors,
|
...PaperDefaultTheme.colors,
|
||||||
},
|
},
|
||||||
};
|
}
|
||||||
|
|
||||||
export const CombinedDarkTheme = {
|
export const CombinedDarkTheme = {
|
||||||
...NavigationDarkTheme,
|
...NavigationDarkTheme,
|
||||||
|
@ -35,61 +34,85 @@ export const CombinedDarkTheme = {
|
||||||
colors: {
|
colors: {
|
||||||
...NavigationDarkTheme.colors,
|
...NavigationDarkTheme.colors,
|
||||||
...PaperDarkTheme.colors,
|
...PaperDarkTheme.colors,
|
||||||
primary: lightColors[0].hex,
|
|
||||||
background: '#0E0E0E',
|
|
||||||
},
|
},
|
||||||
};
|
}
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
const isDark = useColorScheme() === 'dark';
|
const isDark = useColorScheme() === 'dark'
|
||||||
const [settings, setSettings] = useState<Settings>();
|
const [initialized, setInitialized] = useState(false)
|
||||||
const [color, setColor] = useState(
|
const [snackbar, setSnackbar] = useState('')
|
||||||
|
const [theme, setTheme] = useState('system')
|
||||||
|
|
||||||
|
const [color, setColor] = useState<string>(
|
||||||
isDark
|
isDark
|
||||||
? CombinedDarkTheme.colors.primary.toUpperCase()
|
? CombinedDarkTheme.colors.primary
|
||||||
: CombinedDefaultTheme.colors.primary.toUpperCase(),
|
: CombinedDefaultTheme.colors.primary,
|
||||||
);
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
runMigrations().then(async () => {
|
DeviceEventEmitter.addListener(TOAST, ({value}: {value: string}) => {
|
||||||
const gotSettings = await getSettings();
|
console.log(`${Routes.name}.toast:`, {value})
|
||||||
console.log(`${App.name}.runMigrations:`, {gotSettings});
|
setSnackbar(value)
|
||||||
setSettings(gotSettings);
|
})
|
||||||
if (gotSettings.color) setColor(gotSettings.color);
|
if (AppDataSource.isInitialized) return setInitialized(true)
|
||||||
});
|
AppDataSource.initialize().then(async () => {
|
||||||
}, [setColor]);
|
const settings = await settingsRepo.findOne({where: {}})
|
||||||
|
console.log(`${App.name}.useEffect:`, {gotSettings: settings})
|
||||||
|
setTheme(settings.theme)
|
||||||
|
setColor(settings.color)
|
||||||
|
setInitialized(true)
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
const theme = useMemo(() => {
|
const paperTheme = useMemo(() => {
|
||||||
const darkTheme = {
|
const darkTheme = color
|
||||||
|
? {
|
||||||
...CombinedDarkTheme,
|
...CombinedDarkTheme,
|
||||||
colors: {...CombinedDarkTheme.colors, primary: color},
|
colors: {...CombinedDarkTheme.colors, primary: color},
|
||||||
};
|
}
|
||||||
const lightTheme = {
|
: CombinedDarkTheme
|
||||||
|
const lightTheme = color
|
||||||
|
? {
|
||||||
...CombinedDefaultTheme,
|
...CombinedDefaultTheme,
|
||||||
colors: {...CombinedDefaultTheme.colors, primary: color},
|
colors: {...CombinedDefaultTheme.colors, primary: color},
|
||||||
};
|
}
|
||||||
let value = isDark ? darkTheme : lightTheme;
|
: CombinedDefaultTheme
|
||||||
if (settings?.theme === 'dark') value = darkTheme;
|
let value = isDark ? darkTheme : lightTheme
|
||||||
else if (settings?.theme === 'light') value = lightTheme;
|
if (theme === 'dark') value = darkTheme
|
||||||
return value;
|
else if (theme === 'light') value = lightTheme
|
||||||
}, [color, isDark, settings]);
|
return value
|
||||||
|
}, [isDark, theme, color])
|
||||||
|
|
||||||
|
const action = useMemo(
|
||||||
|
() => ({
|
||||||
|
label: 'Close',
|
||||||
|
onPress: () => setSnackbar(''),
|
||||||
|
color: paperTheme.colors.background,
|
||||||
|
}),
|
||||||
|
[paperTheme.colors.background],
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Color.Provider value={{color, setColor}}>
|
<PaperProvider
|
||||||
<Provider
|
theme={paperTheme}
|
||||||
theme={theme}
|
|
||||||
settings={{icon: props => <MaterialIcon {...props} />}}>
|
settings={{icon: props => <MaterialIcon {...props} />}}>
|
||||||
<NavigationContainer theme={theme}>
|
<NavigationContainer theme={paperTheme}>
|
||||||
<MassiveSnack>
|
{initialized && (
|
||||||
{settings && (
|
<ThemeContext.Provider value={{theme, setTheme, color, setColor}}>
|
||||||
<SettingsContext.Provider value={{settings, setSettings}}>
|
|
||||||
<Routes />
|
<Routes />
|
||||||
</SettingsContext.Provider>
|
</ThemeContext.Provider>
|
||||||
)}
|
)}
|
||||||
</MassiveSnack>
|
|
||||||
</NavigationContainer>
|
</NavigationContainer>
|
||||||
</Provider>
|
|
||||||
</Color.Provider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default App;
|
<Snackbar
|
||||||
|
duration={3000}
|
||||||
|
onDismiss={() => setSnackbar('')}
|
||||||
|
visible={!!snackbar}
|
||||||
|
action={action}>
|
||||||
|
{snackbar}
|
||||||
|
</Snackbar>
|
||||||
|
</PaperProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App
|
||||||
|
|
95
BestList.tsx
|
@ -2,45 +2,70 @@ import {
|
||||||
NavigationProp,
|
NavigationProp,
|
||||||
useFocusEffect,
|
useFocusEffect,
|
||||||
useNavigation,
|
useNavigation,
|
||||||
} from '@react-navigation/native';
|
} from '@react-navigation/native'
|
||||||
import React, {useCallback, useEffect, 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 {getBestReps, getBestWeights} from './best.service';
|
import {BestPageParams} from './BestPage'
|
||||||
import {BestPageParams} from './BestPage';
|
import {setRepo, settingsRepo} from './db'
|
||||||
import DrawerHeader from './DrawerHeader';
|
import DrawerHeader from './DrawerHeader'
|
||||||
import Page from './Page';
|
import GymSet from './gym-set'
|
||||||
import Set from './set';
|
import Page from './Page'
|
||||||
import {useSettings} from './use-settings';
|
import Settings from './settings'
|
||||||
|
|
||||||
export default function BestList() {
|
export default function BestList() {
|
||||||
const [bests, setBests] = useState<Set[]>();
|
const [bests, setBests] = useState<GymSet[]>()
|
||||||
const [search, setSearch] = useState('');
|
const [term, setTerm] = useState('')
|
||||||
const navigation = useNavigation<NavigationProp<BestPageParams>>();
|
const navigation = useNavigation<NavigationProp<BestPageParams>>()
|
||||||
const {settings} = useSettings();
|
const [settings, setSettings] = useState<Settings>()
|
||||||
|
|
||||||
const refresh = useCallback(async () => {
|
|
||||||
const weights = await getBestWeights(search);
|
|
||||||
console.log(`${BestList.name}.refresh:`, {length: weights.length});
|
|
||||||
let newBest: Set[] = [];
|
|
||||||
for (const set of weights) {
|
|
||||||
const reps = await getBestReps(set.name, set.weight);
|
|
||||||
newBest.push(...reps);
|
|
||||||
}
|
|
||||||
setBests(newBest);
|
|
||||||
}, [search]);
|
|
||||||
|
|
||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
refresh();
|
settingsRepo.findOne({where: {}}).then(setSettings)
|
||||||
}, [refresh]),
|
}, []),
|
||||||
);
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
const refresh = useCallback(async (value: string) => {
|
||||||
refresh();
|
const weights = await setRepo
|
||||||
}, [search, refresh]);
|
.createQueryBuilder()
|
||||||
|
.select()
|
||||||
|
.addSelect('MAX(weight)', 'weight')
|
||||||
|
.where('name LIKE :name', {name: `%${value}%`})
|
||||||
|
.andWhere('NOT hidden')
|
||||||
|
.groupBy('name')
|
||||||
|
.getMany()
|
||||||
|
console.log(`${BestList.name}.refresh:`, {length: weights.length})
|
||||||
|
let newBest: GymSet[] = []
|
||||||
|
for (const set of weights) {
|
||||||
|
const reps = await setRepo
|
||||||
|
.createQueryBuilder()
|
||||||
|
.select()
|
||||||
|
.addSelect('MAX(reps)', 'reps')
|
||||||
|
.where('name = :name', {name: set.name})
|
||||||
|
.andWhere('weight = :weight', {weight: set.weight})
|
||||||
|
.andWhere('NOT hidden')
|
||||||
|
.groupBy('name')
|
||||||
|
.getMany()
|
||||||
|
newBest.push(...reps)
|
||||||
|
}
|
||||||
|
setBests(newBest)
|
||||||
|
}, [])
|
||||||
|
|
||||||
const renderItem = ({item}: {item: Set}) => (
|
useFocusEffect(
|
||||||
|
useCallback(() => {
|
||||||
|
refresh(term)
|
||||||
|
}, [refresh, term]),
|
||||||
|
)
|
||||||
|
|
||||||
|
const search = useCallback(
|
||||||
|
(value: string) => {
|
||||||
|
setTerm(value)
|
||||||
|
refresh(value)
|
||||||
|
},
|
||||||
|
[refresh],
|
||||||
|
)
|
||||||
|
|
||||||
|
const renderItem = ({item}: {item: GymSet}) => (
|
||||||
<List.Item
|
<List.Item
|
||||||
key={item.name}
|
key={item.name}
|
||||||
title={item.name}
|
title={item.name}
|
||||||
|
@ -53,12 +78,12 @@ export default function BestList() {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
);
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DrawerHeader name="Best" />
|
<DrawerHeader name="Best" />
|
||||||
<Page search={search} setSearch={setSearch}>
|
<Page term={term} search={search}>
|
||||||
{bests?.length === 0 ? (
|
{bests?.length === 0 ? (
|
||||||
<List.Item
|
<List.Item
|
||||||
title="No exercises yet"
|
title="No exercises yet"
|
||||||
|
@ -69,5 +94,5 @@ export default function BestList() {
|
||||||
)}
|
)}
|
||||||
</Page>
|
</Page>
|
||||||
</>
|
</>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
21
BestPage.tsx
|
@ -1,16 +1,15 @@
|
||||||
import {createStackNavigator} from '@react-navigation/stack';
|
import {createStackNavigator} from '@react-navigation/stack'
|
||||||
import React from 'react';
|
import BestList from './BestList'
|
||||||
import BestList from './BestList';
|
import GymSet from './gym-set'
|
||||||
import Set from './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: Set;
|
best: GymSet
|
||||||
};
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
export default function BestPage() {
|
export default function BestPage() {
|
||||||
return (
|
return (
|
||||||
|
@ -19,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>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
39
Chart.tsx
|
@ -1,12 +1,11 @@
|
||||||
import * as shape from 'd3-shape';
|
import {useTheme} from '@react-navigation/native'
|
||||||
import React from 'react';
|
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 {MARGIN, PADDING} from './constants'
|
||||||
import {MARGIN, PADDING} from './constants';
|
import GymSet from './gym-set'
|
||||||
import Set from './set';
|
import useDark from './use-dark'
|
||||||
import useDark from './use-dark';
|
|
||||||
|
|
||||||
export default function Chart({
|
export default function Chart({
|
||||||
yData,
|
yData,
|
||||||
|
@ -14,21 +13,21 @@ export default function Chart({
|
||||||
xData,
|
xData,
|
||||||
yFormat,
|
yFormat,
|
||||||
}: {
|
}: {
|
||||||
yData: number[];
|
yData: number[]
|
||||||
xData: Set[];
|
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 {colors} = useTheme()
|
||||||
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 (
|
||||||
<>
|
<>
|
||||||
|
@ -47,7 +46,7 @@ export default function Chart({
|
||||||
contentInset={verticalContentInset}
|
contentInset={verticalContentInset}
|
||||||
curve={shape.curveBasis}
|
curve={shape.curveBasis}
|
||||||
svg={{
|
svg={{
|
||||||
stroke: color,
|
stroke: colors.primary,
|
||||||
}}>
|
}}>
|
||||||
<Grid />
|
<Grid />
|
||||||
</LineChart>
|
</LineChart>
|
||||||
|
@ -61,5 +60,5 @@ export default function Chart({
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</>
|
</>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import React from 'react';
|
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,
|
||||||
|
@ -8,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>
|
||||||
|
@ -27,5 +26,5 @@ export default function ConfirmDialog({
|
||||||
</Dialog.Actions>
|
</Dialog.Actions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</Portal>
|
</Portal>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,23 @@
|
||||||
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 React from 'react';
|
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'
|
||||||
|
|
||||||
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()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Appbar.Header>
|
<Appbar.Header>
|
||||||
<IconButton icon="menu" onPress={navigation.openDrawer} />
|
<IconButton
|
||||||
|
color={dark ? 'white' : 'white'}
|
||||||
|
icon="menu"
|
||||||
|
onPress={navigation.openDrawer}
|
||||||
|
/>
|
||||||
<Appbar.Content title={name} />
|
<Appbar.Content title={name} />
|
||||||
<DrawerMenu name={name} />
|
<DrawerMenu name={name} />
|
||||||
</Appbar.Header>
|
</Appbar.Header>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
183
DrawerMenu.tsx
|
@ -1,67 +1,75 @@
|
||||||
import {NavigationProp, useNavigation} from '@react-navigation/native';
|
import {NavigationProp, useNavigation} from '@react-navigation/native'
|
||||||
import React, {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 {DrawerParamList} from './drawer-param-list';
|
import {AppDataSource} from './data-source'
|
||||||
import {useSnackbar} from './MassiveSnack';
|
import {planRepo} from './db'
|
||||||
import {Plan} from './plan';
|
import {DrawerParamList} from './drawer-param-list'
|
||||||
import {addPlans, deletePlans, getAllPlans} from './plan.service';
|
import GymSet from './gym-set'
|
||||||
import {addSets, deleteSets, getAllSets} from './set.service';
|
import {Plan} from './plan'
|
||||||
import {write} from './write';
|
import {toast} from './toast'
|
||||||
|
import useDark from './use-dark'
|
||||||
|
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)
|
||||||
|
|
||||||
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 {reset} = useNavigation<NavigationProp<DrawerParamList>>()
|
||||||
const {reset} = useNavigation<NavigationProp<DrawerParamList>>();
|
const dark = useDark()
|
||||||
|
|
||||||
const exportSets = useCallback(async () => {
|
const exportSets = useCallback(async () => {
|
||||||
const sets = await getAllSets();
|
const sets = await setRepo.find({})
|
||||||
const data = [setFields]
|
const data = [setFields]
|
||||||
.concat(
|
.concat(
|
||||||
sets.map(
|
sets.map(set =>
|
||||||
set =>
|
setFields
|
||||||
`${set.id},${set.name},${set.reps},${set.weight},${set.created},${set.unit},${set.hidden},${set.sets},${set.minutes},${set.seconds}`,
|
.split(',')
|
||||||
|
.map(fieldString => {
|
||||||
|
const field = fieldString as keyof GymSet
|
||||||
|
if (field === 'unit') return set[field] || 'kg'
|
||||||
|
return set[field]
|
||||||
|
})
|
||||||
|
.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: Plan[] = await getAllPlans();
|
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.name}:`, 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.')
|
||||||
const values = lines
|
const values = lines
|
||||||
.slice(1)
|
.slice(1)
|
||||||
.filter(line => line)
|
.filter(line => line)
|
||||||
.map(set => {
|
.map(line => {
|
||||||
const [
|
let [
|
||||||
,
|
,
|
||||||
setName,
|
setName,
|
||||||
reps,
|
reps,
|
||||||
|
@ -72,23 +80,33 @@ export default function DrawerMenu({name}: {name: keyof DrawerParamList}) {
|
||||||
sets,
|
sets,
|
||||||
minutes,
|
minutes,
|
||||||
seconds,
|
seconds,
|
||||||
] = set.split(',');
|
] = line.split(',')
|
||||||
return `('${setName}',${reps},${weight},'${created}','${unit}',${hidden},${
|
const set: GymSet = {
|
||||||
sets ?? 3
|
name: setName,
|
||||||
},${minutes ?? 3},${seconds ?? 30})`;
|
reps: +reps,
|
||||||
|
weight: +weight,
|
||||||
|
created,
|
||||||
|
unit: unit ?? 'kg',
|
||||||
|
hidden: !!Number(hidden),
|
||||||
|
sets: +sets,
|
||||||
|
minutes: +minutes,
|
||||||
|
seconds: +seconds,
|
||||||
|
image: '',
|
||||||
|
}
|
||||||
|
return set
|
||||||
})
|
})
|
||||||
.join(',');
|
console.log(`${DrawerMenu.name}.uploadSets:`, {values})
|
||||||
await addSets(setFields.split(',').slice(1).join(','), values);
|
await setRepo.insert(values)
|
||||||
toast('Data imported.', 3000);
|
toast('Data imported.')
|
||||||
reset({index: 0, routes: [{name}]});
|
reset({index: 0, routes: [{name}]})
|
||||||
}, [reset, name, toast]);
|
}, [reset, name])
|
||||||
|
|
||||||
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.')
|
||||||
const values = file
|
const values = file
|
||||||
.split('\n')
|
.split('\n')
|
||||||
.slice(1)
|
.slice(1)
|
||||||
|
@ -96,29 +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, ''))
|
||||||
return `('${days}','${workouts}')`;
|
const plan: Plan = {
|
||||||
|
days,
|
||||||
|
workouts,
|
||||||
|
}
|
||||||
|
return plan
|
||||||
})
|
})
|
||||||
.join(',');
|
await planRepo.insert(values)
|
||||||
await addPlans(values);
|
toast('Data imported.')
|
||||||
toast('Data imported.', 3000);
|
}, [])
|
||||||
}, [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 deleteSets();
|
if (name === 'Home') await setRepo.delete({})
|
||||||
else if (name === 'Plans') await deletePlans();
|
else if (name === 'Plans') await planRepo.delete({})
|
||||||
toast('All data has been deleted.', 4000);
|
toast('All data has been deleted.')
|
||||||
reset({index: 0, routes: [{name}]});
|
reset({index: 0, routes: [{name}]})
|
||||||
}, [reset, name, toast]);
|
}, [reset, name])
|
||||||
|
|
||||||
if (name === 'Home' || name === 'Plans')
|
if (name === 'Home' || name === 'Plans')
|
||||||
return (
|
return (
|
||||||
|
@ -126,7 +147,11 @@ export default function DrawerMenu({name}: {name: keyof DrawerParamList}) {
|
||||||
visible={showMenu}
|
visible={showMenu}
|
||||||
onDismiss={() => setShowMenu(false)}
|
onDismiss={() => setShowMenu(false)}
|
||||||
anchor={
|
anchor={
|
||||||
<IconButton onPress={() => setShowMenu(true)} icon="more-vert" />
|
<IconButton
|
||||||
|
color={dark ? 'white' : 'white'}
|
||||||
|
onPress={() => setShowMenu(true)}
|
||||||
|
icon="more-vert"
|
||||||
|
/>
|
||||||
}>
|
}>
|
||||||
<Menu.Item icon="arrow-downward" onPress={download} title="Download" />
|
<Menu.Item icon="arrow-downward" onPress={download} title="Download" />
|
||||||
<Menu.Item icon="arrow-upward" onPress={upload} title="Upload" />
|
<Menu.Item icon="arrow-upward" onPress={upload} title="Upload" />
|
||||||
|
@ -144,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
|
||||||
}
|
}
|
||||||
|
|
91
EditPlan.tsx
|
@ -3,75 +3,72 @@ import {
|
||||||
RouteProp,
|
RouteProp,
|
||||||
useNavigation,
|
useNavigation,
|
||||||
useRoute,
|
useRoute,
|
||||||
} from '@react-navigation/native';
|
} from '@react-navigation/native'
|
||||||
import React, {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 {DrawerParamList} from './drawer-param-list';
|
import {planRepo, setRepo} from './db'
|
||||||
import {PlanPageParams} from './plan-page-params';
|
import {DrawerParamList} from './drawer-param-list'
|
||||||
import {addPlan, updatePlan} from './plan.service';
|
import {PlanPageParams} from './plan-page-params'
|
||||||
import {getNames} from './set.service';
|
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(() => {
|
||||||
getNames().then(n => {
|
setRepo
|
||||||
console.log(EditPlan.name, {n});
|
.createQueryBuilder()
|
||||||
setNames(n);
|
.select('name')
|
||||||
});
|
.distinct(true)
|
||||||
}, []);
|
.getRawMany()
|
||||||
|
.then(values => {
|
||||||
|
console.log(EditPlan.name, {values})
|
||||||
|
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(',')
|
||||||
if (typeof plan.id === 'undefined')
|
await planRepo.save({days: newDays, workouts: newWorkouts, id: plan.id})
|
||||||
await addPlan({days: newDays, workouts: newWorkouts});
|
navigation.goBack()
|
||||||
else
|
}, [days, workouts, plan, navigation])
|
||||||
await updatePlan({
|
|
||||||
days: newDays,
|
|
||||||
workouts: newWorkouts,
|
|
||||||
id: plan.id,
|
|
||||||
});
|
|
||||||
navigation.goBack();
|
|
||||||
}, [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 (
|
||||||
<>
|
<>
|
||||||
|
@ -110,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>
|
||||||
|
@ -130,7 +127,7 @@ export default function EditPlan() {
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
</>
|
</>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
|
@ -138,4 +135,4 @@ const styles = StyleSheet.create({
|
||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
marginBottom: MARGIN,
|
marginBottom: MARGIN,
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
|
103
EditSet.tsx
|
@ -1,81 +1,78 @@
|
||||||
import {RouteProp, useNavigation, useRoute} from '@react-navigation/native';
|
import {
|
||||||
import React, {useCallback} from 'react';
|
RouteProp,
|
||||||
import {NativeModules, View} from 'react-native';
|
useFocusEffect,
|
||||||
import {PADDING} from './constants';
|
useNavigation,
|
||||||
import {HomePageParams} from './home-page-params';
|
useRoute,
|
||||||
import {useSnackbar} from './MassiveSnack';
|
} from '@react-navigation/native'
|
||||||
import Set from './set';
|
import {useCallback, useState} from 'react'
|
||||||
import {addSet, getSet, updateSet} from './set.service';
|
import {NativeModules, View} from 'react-native'
|
||||||
import SetForm from './SetForm';
|
import {PADDING} from './constants'
|
||||||
import {updateSettings} from './settings.service';
|
import {setRepo, settingsRepo} from './db'
|
||||||
import StackHeader from './StackHeader';
|
import GymSet from './gym-set'
|
||||||
import {useSettings} from './use-settings';
|
import {HomePageParams} from './home-page-params'
|
||||||
|
import SetForm from './SetForm'
|
||||||
|
import Settings from './settings'
|
||||||
|
import StackHeader from './StackHeader'
|
||||||
|
import {toast} from './toast'
|
||||||
|
|
||||||
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 [settings, setSettings] = useState<Settings>()
|
||||||
const {settings} = useSettings();
|
|
||||||
|
useFocusEffect(
|
||||||
|
useCallback(() => {
|
||||||
|
settingsRepo.findOne({where: {}}).then(setSettings)
|
||||||
|
}, []),
|
||||||
|
)
|
||||||
|
|
||||||
const startTimer = useCallback(
|
const startTimer = useCallback(
|
||||||
async (name: string) => {
|
async (name: string) => {
|
||||||
if (!settings.alarm) return;
|
if (!settings.alarm) return
|
||||||
const {minutes, seconds} = await getSet(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
|
||||||
console.log(`startTimer:`, `Starting timer in ${minutes}:${seconds}`);
|
|
||||||
NativeModules.AlarmModule.timer(
|
NativeModules.AlarmModule.timer(
|
||||||
milliseconds,
|
milliseconds,
|
||||||
!!settings.vibrate,
|
settings.vibrate,
|
||||||
settings.sound,
|
settings.sound,
|
||||||
!!settings.noSound,
|
settings.noSound,
|
||||||
);
|
)
|
||||||
const nextAlarm = new Date();
|
|
||||||
nextAlarm.setTime(nextAlarm.getTime() + milliseconds);
|
|
||||||
updateSettings({...settings, nextAlarm: nextAlarm.toISOString()});
|
|
||||||
},
|
},
|
||||||
[settings],
|
[settings],
|
||||||
);
|
)
|
||||||
|
|
||||||
const update = useCallback(
|
|
||||||
async (value: Set) => {
|
|
||||||
console.log(`${EditSet.name}.update`, value);
|
|
||||||
await updateSet(value);
|
|
||||||
navigation.goBack();
|
|
||||||
},
|
|
||||||
[navigation],
|
|
||||||
);
|
|
||||||
|
|
||||||
const add = useCallback(
|
const add = useCallback(
|
||||||
async (value: Set) => {
|
async (value: GymSet) => {
|
||||||
console.log(`${EditSet.name}.add`, {set: value});
|
startTimer(value.name)
|
||||||
startTimer(value.name);
|
console.log(`${EditSet.name}.add`, {set: value})
|
||||||
await addSet(value);
|
const result = await setRepo.save(value)
|
||||||
if (!settings.notify) return navigation.goBack();
|
console.log({result})
|
||||||
|
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.")
|
||||||
navigation.goBack();
|
|
||||||
},
|
},
|
||||||
[navigation, startTimer, set, toast, settings],
|
[startTimer, set, settings],
|
||||||
);
|
)
|
||||||
|
|
||||||
const save = useCallback(
|
const save = useCallback(
|
||||||
async (value: Set) => {
|
async (value: GymSet) => {
|
||||||
if (typeof set.id === 'number') return update(value);
|
if (typeof set.id === 'number') await setRepo.save(value)
|
||||||
return add(value);
|
else await add(value)
|
||||||
|
navigation.goBack()
|
||||||
},
|
},
|
||||||
[update, add, set.id],
|
[add, set.id, navigation],
|
||||||
);
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<StackHeader title="Edit set" />
|
<StackHeader title="Edit set" />
|
||||||
<View style={{padding: PADDING, flex: 1}}>
|
<View style={{padding: PADDING, flex: 1}}>
|
||||||
<SetForm save={save} set={set} />
|
{settings && <SetForm settings={settings} save={save} set={set} />}
|
||||||
</View>
|
</View>
|
||||||
</>
|
</>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
158
EditWorkout.tsx
|
@ -1,56 +1,73 @@
|
||||||
import {RouteProp, useNavigation, useRoute} from '@react-navigation/native';
|
import {
|
||||||
import React, {useCallback, useRef, useState} from 'react';
|
RouteProp,
|
||||||
import {ScrollView, TextInput, View} from 'react-native';
|
useFocusEffect,
|
||||||
import DocumentPicker from 'react-native-document-picker';
|
useNavigation,
|
||||||
import {Button, Card, TouchableRipple} from 'react-native-paper';
|
useRoute,
|
||||||
import ConfirmDialog from './ConfirmDialog';
|
} from '@react-navigation/native'
|
||||||
import {MARGIN, PADDING} from './constants';
|
import {useCallback, useRef, useState} from 'react'
|
||||||
import MassiveInput from './MassiveInput';
|
import {ScrollView, TextInput, View} from 'react-native'
|
||||||
import {useSnackbar} from './MassiveSnack';
|
import DocumentPicker from 'react-native-document-picker'
|
||||||
import {updatePlanWorkouts} from './plan.service';
|
import {Button, Card, TouchableRipple} from 'react-native-paper'
|
||||||
import {addSet, updateManySet, updateSetImage} from './set.service';
|
import ConfirmDialog from './ConfirmDialog'
|
||||||
import StackHeader from './StackHeader';
|
import {MARGIN, PADDING} from './constants'
|
||||||
import {useSettings} from './use-settings';
|
import {getNow, planRepo, setRepo, settingsRepo} from './db'
|
||||||
import {WorkoutsPageParams} from './WorkoutsPage';
|
import MassiveInput from './MassiveInput'
|
||||||
|
import Settings from './settings'
|
||||||
|
import StackHeader from './StackHeader'
|
||||||
|
import {toast} from './toast'
|
||||||
|
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 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, setSettings] = useState<Settings>()
|
||||||
const {settings} = useSettings();
|
|
||||||
|
useFocusEffect(
|
||||||
|
useCallback(() => {
|
||||||
|
settingsRepo.findOne({where: {}}).then(setSettings)
|
||||||
|
}, []),
|
||||||
|
)
|
||||||
|
|
||||||
const update = async () => {
|
const update = async () => {
|
||||||
await updateManySet({
|
await setRepo.update(
|
||||||
oldName: params.value.name,
|
{name: params.value.name},
|
||||||
newName: name || params.value.name,
|
{
|
||||||
sets: sets ?? '3',
|
name: name || params.value.name,
|
||||||
seconds: seconds?.toString() ?? '30',
|
sets: Number(sets),
|
||||||
minutes: minutes?.toString() ?? '3',
|
minutes: +minutes,
|
||||||
|
seconds: +seconds,
|
||||||
steps,
|
steps,
|
||||||
});
|
image: removeImage ? '' : uri,
|
||||||
await updatePlanWorkouts(params.value.name, name || params.value.name);
|
},
|
||||||
if (uri || removeImage) await updateSetImage(params.value.name, uri || '');
|
)
|
||||||
navigation.goBack();
|
await planRepo.query(
|
||||||
};
|
`UPDATE plans
|
||||||
|
SET workouts = REPLACE(workouts, $1, $2)
|
||||||
|
WHERE workouts LIKE $3`,
|
||||||
|
[params.value.name, name, `%${params.value.name}%`],
|
||||||
|
)
|
||||||
|
navigation.goBack()
|
||||||
|
}
|
||||||
|
|
||||||
const add = async () => {
|
const add = async () => {
|
||||||
await addSet({
|
const [{now}] = await getNow()
|
||||||
|
await setRepo.save({
|
||||||
name,
|
name,
|
||||||
reps: 0,
|
reps: 0,
|
||||||
weight: 0,
|
weight: 0,
|
||||||
|
@ -60,45 +77,46 @@ export default function EditWorkout() {
|
||||||
seconds: seconds ? +seconds : 30,
|
seconds: seconds ? +seconds : 30,
|
||||||
sets: sets ? +sets : 3,
|
sets: sets ? +sets : 3,
|
||||||
steps,
|
steps,
|
||||||
});
|
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')
|
||||||
};
|
}
|
||||||
|
|
||||||
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')
|
||||||
};
|
}
|
||||||
|
|
||||||
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 (
|
||||||
<>
|
<>
|
||||||
|
@ -112,7 +130,7 @@ export default function EditWorkout() {
|
||||||
onChangeText={handleName}
|
onChangeText={handleName}
|
||||||
onSubmitEditing={submitName}
|
onSubmitEditing={submitName}
|
||||||
/>
|
/>
|
||||||
{!!settings.steps && (
|
{settings?.steps && (
|
||||||
<MassiveInput
|
<MassiveInput
|
||||||
innerRef={stepsRef}
|
innerRef={stepsRef}
|
||||||
selectTextOnFocus={false}
|
selectTextOnFocus={false}
|
||||||
|
@ -123,7 +141,7 @@ export default function EditWorkout() {
|
||||||
onSubmitEditing={() => setsRef.current?.focus()}
|
onSubmitEditing={() => setsRef.current?.focus()}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!!settings.showSets && (
|
{settings?.showSets && (
|
||||||
<MassiveInput
|
<MassiveInput
|
||||||
innerRef={setsRef}
|
innerRef={setsRef}
|
||||||
value={sets}
|
value={sets}
|
||||||
|
@ -133,7 +151,7 @@ export default function EditWorkout() {
|
||||||
onSubmitEditing={() => minutesRef.current?.focus()}
|
onSubmitEditing={() => minutesRef.current?.focus()}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!!settings.alarm && (
|
{settings?.alarm && (
|
||||||
<>
|
<>
|
||||||
<MassiveInput
|
<MassiveInput
|
||||||
innerRef={minutesRef}
|
innerRef={minutesRef}
|
||||||
|
@ -153,7 +171,7 @@ export default function EditWorkout() {
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{!!settings.images && uri && (
|
{settings?.images && uri && (
|
||||||
<TouchableRipple
|
<TouchableRipple
|
||||||
style={{marginBottom: MARGIN}}
|
style={{marginBottom: MARGIN}}
|
||||||
onPress={changeImage}
|
onPress={changeImage}
|
||||||
|
@ -161,7 +179,7 @@ export default function EditWorkout() {
|
||||||
<Card.Cover source={{uri}} />
|
<Card.Cover source={{uri}} />
|
||||||
</TouchableRipple>
|
</TouchableRipple>
|
||||||
)}
|
)}
|
||||||
{!!settings.images && !uri && (
|
{settings?.images && !uri && (
|
||||||
<Button
|
<Button
|
||||||
style={{marginBottom: MARGIN}}
|
style={{marginBottom: MARGIN}}
|
||||||
onPress={changeImage}
|
onPress={changeImage}
|
||||||
|
@ -182,5 +200,5 @@ export default function EditWorkout() {
|
||||||
</ConfirmDialog>
|
</ConfirmDialog>
|
||||||
</View>
|
</View>
|
||||||
</>
|
</>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
13
HomePage.tsx
|
@ -1,10 +1,9 @@
|
||||||
import {createStackNavigator} from '@react-navigation/stack';
|
import {createStackNavigator} from '@react-navigation/stack'
|
||||||
import React from 'react';
|
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 (
|
||||||
|
@ -13,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>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
import React from 'react';
|
import {ComponentProps} from 'react'
|
||||||
import {FAB} from 'react-native-paper';
|
import {FAB} from 'react-native-paper'
|
||||||
import {useColor} from './color';
|
import {CombinedDarkTheme, CombinedDefaultTheme} from './App'
|
||||||
import {lightColors} from './colors';
|
import {lightColors} from './colors'
|
||||||
|
import {useTheme} from './use-theme'
|
||||||
|
|
||||||
export default function MassiveFab(
|
export default function MassiveFab(props: Partial<ComponentProps<typeof FAB>>) {
|
||||||
props: Partial<React.ComponentProps<typeof FAB>>,
|
const {color} = useTheme()
|
||||||
) {
|
|
||||||
const {color} = useColor();
|
const fabColor = lightColors.includes(color)
|
||||||
const fabColor = lightColors.map(lightColor => lightColor.hex).includes(color)
|
? CombinedDarkTheme.colors.background
|
||||||
? 'black'
|
: CombinedDefaultTheme.colors.background
|
||||||
: undefined;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FAB
|
<FAB
|
||||||
|
@ -23,5 +23,5 @@ export default function MassiveFab(
|
||||||
}}
|
}}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
import React 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<React.ComponentProps<typeof TextInput>> & {
|
props: Partial<ComponentProps<typeof TextInput>> & {
|
||||||
innerRef?: React.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}
|
||||||
/>
|
/>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,49 +0,0 @@
|
||||||
import React, {useContext, useState} from 'react';
|
|
||||||
import {Snackbar} from 'react-native-paper';
|
|
||||||
import {CombinedDarkTheme, CombinedDefaultTheme} from './App';
|
|
||||||
import useDark from './use-dark';
|
|
||||||
|
|
||||||
export const SnackbarContext = React.createContext<{
|
|
||||||
toast: (value: string, timeout: number) => void;
|
|
||||||
}>({toast: () => null});
|
|
||||||
|
|
||||||
export const useSnackbar = () => {
|
|
||||||
return useContext(SnackbarContext);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function MassiveSnack({
|
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
children?: JSX.Element[] | JSX.Element;
|
|
||||||
}) {
|
|
||||||
const [snackbar, setSnackbar] = useState('');
|
|
||||||
const [timeoutId, setTimeoutId] = useState(0);
|
|
||||||
const dark = useDark();
|
|
||||||
|
|
||||||
const toast = (value: string, timeout: number) => {
|
|
||||||
setSnackbar(value);
|
|
||||||
clearTimeout(timeoutId);
|
|
||||||
const id = setTimeout(() => setSnackbar(''), timeout);
|
|
||||||
setTimeoutId(id);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<SnackbarContext.Provider value={{toast}}>
|
|
||||||
{children}
|
|
||||||
</SnackbarContext.Provider>
|
|
||||||
<Snackbar
|
|
||||||
onDismiss={() => setSnackbar('')}
|
|
||||||
visible={!!snackbar}
|
|
||||||
action={{
|
|
||||||
label: 'Close',
|
|
||||||
onPress: () => setSnackbar(''),
|
|
||||||
color: dark
|
|
||||||
? CombinedDarkTheme.colors.background
|
|
||||||
: CombinedDefaultTheme.colors.background,
|
|
||||||
}}>
|
|
||||||
{snackbar}
|
|
||||||
</Snackbar>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
29
Page.tsx
|
@ -1,35 +1,32 @@
|
||||||
import React from 'react';
|
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,
|
||||||
children,
|
children,
|
||||||
|
term,
|
||||||
search,
|
search,
|
||||||
setSearch,
|
|
||||||
}: {
|
}: {
|
||||||
children: JSX.Element | JSX.Element[];
|
children: JSX.Element | JSX.Element[]
|
||||||
onAdd?: () => void;
|
onAdd?: () => void
|
||||||
search?: string;
|
term: string
|
||||||
setSearch?: (value: string) => void;
|
search: (value: string) => void
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
{typeof search === 'string' && setSearch && (
|
|
||||||
<Searchbar
|
<Searchbar
|
||||||
placeholder="Search"
|
placeholder="Search"
|
||||||
value={search}
|
value={term}
|
||||||
onChangeText={setSearch}
|
onChangeText={search}
|
||||||
icon="search"
|
icon="search"
|
||||||
clearIcon="clear"
|
clearIcon="clear"
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
{children}
|
{children}
|
||||||
{onAdd && <MassiveFab onPress={onAdd} />}
|
{onAdd && <MassiveFab onPress={onAdd} />}
|
||||||
</View>
|
</View>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
|
@ -37,4 +34,4 @@ const styles = StyleSheet.create({
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
padding: PADDING,
|
padding: PADDING,
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
|
83
PlanItem.tsx
|
@ -2,62 +2,59 @@ import {
|
||||||
NavigationProp,
|
NavigationProp,
|
||||||
useFocusEffect,
|
useFocusEffect,
|
||||||
useNavigation,
|
useNavigation,
|
||||||
} from '@react-navigation/native';
|
} from '@react-navigation/native'
|
||||||
import React, {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 {planRepo} from './db'
|
||||||
import {Plan} from './plan';
|
import {Plan} from './plan'
|
||||||
import {PlanPageParams} from './plan-page-params';
|
import {PlanPageParams} from './plan-page-params'
|
||||||
import {deletePlan} from './plan.service';
|
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 deletePlan(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 () => {
|
||||||
const workouts = item.workouts.split(',');
|
console.log(`${PlanItem.name}.start:`, {item})
|
||||||
const first = workouts[0];
|
setShow(false)
|
||||||
const set = await getBestSet(first);
|
navigation.navigate('StartPlan', {plan: item})
|
||||||
setShow(false);
|
}, [item, navigation])
|
||||||
navigation.navigate('StartPlan', {plan: item, set});
|
|
||||||
}, [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(
|
||||||
() =>
|
() =>
|
||||||
|
@ -74,15 +71,21 @@ 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],
|
||||||
);
|
)
|
||||||
|
|
||||||
|
const copy = useCallback(() => {
|
||||||
|
const plan: Plan = {...item}
|
||||||
|
delete plan.id
|
||||||
|
setShow(false)
|
||||||
|
navigation.navigate('EditPlan', {plan})
|
||||||
|
}, [navigation, item])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
|
||||||
<List.Item
|
<List.Item
|
||||||
onPress={start}
|
onPress={start}
|
||||||
title={title}
|
title={title}
|
||||||
|
@ -91,11 +94,11 @@ export default function PlanItem({
|
||||||
right={() => (
|
right={() => (
|
||||||
<Menu anchor={anchor} visible={show} onDismiss={() => setShow(false)}>
|
<Menu anchor={anchor} visible={show} onDismiss={() => setShow(false)}>
|
||||||
<Menu.Item icon="edit" onPress={edit} title="Edit" />
|
<Menu.Item icon="edit" onPress={edit} title="Edit" />
|
||||||
|
<Menu.Item icon="content-copy" onPress={copy} title="Copy" />
|
||||||
<Divider />
|
<Divider />
|
||||||
<Menu.Item icon="delete" onPress={remove} title="Delete" />
|
<Menu.Item icon="delete" onPress={remove} title="Delete" />
|
||||||
</Menu>
|
</Menu>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</>
|
)
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
65
PlanList.tsx
|
@ -2,50 +2,59 @@ import {
|
||||||
NavigationProp,
|
NavigationProp,
|
||||||
useFocusEffect,
|
useFocusEffect,
|
||||||
useNavigation,
|
useNavigation,
|
||||||
} from '@react-navigation/native';
|
} from '@react-navigation/native'
|
||||||
import React, {useCallback, useEffect, 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 {Like} from 'typeorm'
|
||||||
import Page from './Page';
|
import {planRepo} from './db'
|
||||||
import {Plan} from './plan';
|
import DrawerHeader from './DrawerHeader'
|
||||||
import {PlanPageParams} from './plan-page-params';
|
import Page from './Page'
|
||||||
import {getPlans} from './plan.service';
|
import {Plan} from './plan'
|
||||||
import PlanItem from './PlanItem';
|
import {PlanPageParams} from './plan-page-params'
|
||||||
|
import PlanItem from './PlanItem'
|
||||||
|
|
||||||
export default function PlanList() {
|
export default function PlanList() {
|
||||||
const [search, setSearch] = 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 () => {
|
const refresh = useCallback(async (value: string) => {
|
||||||
getPlans(search).then(setPlans);
|
planRepo
|
||||||
}, [search]);
|
.find({
|
||||||
|
where: [{days: Like(`%${value}%`)}, {workouts: Like(`%${value}%`)}],
|
||||||
|
})
|
||||||
|
.then(setPlans)
|
||||||
|
}, [])
|
||||||
|
|
||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
refresh();
|
refresh(term)
|
||||||
}, [refresh]),
|
}, [refresh, term]),
|
||||||
);
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
const search = useCallback(
|
||||||
refresh();
|
(value: string) => {
|
||||||
}, [search, refresh]);
|
setTerm(value)
|
||||||
|
refresh(value)
|
||||||
|
},
|
||||||
|
[refresh],
|
||||||
|
)
|
||||||
|
|
||||||
const renderItem = useCallback(
|
const renderItem = useCallback(
|
||||||
({item}: {item: Plan}) => (
|
({item}: {item: Plan}) => (
|
||||||
<PlanItem item={item} key={item.id} onRemove={refresh} />
|
<PlanItem item={item} key={item.id} onRemove={() => refresh(term)} />
|
||||||
),
|
),
|
||||||
[refresh],
|
[refresh, term],
|
||||||
);
|
)
|
||||||
|
|
||||||
const onAdd = () =>
|
const onAdd = () =>
|
||||||
navigation.navigate('EditPlan', {plan: {days: '', workouts: ''}});
|
navigation.navigate('EditPlan', {plan: {days: '', workouts: ''}})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DrawerHeader name="Plans" />
|
<DrawerHeader name="Plans" />
|
||||||
<Page onAdd={onAdd} search={search} setSearch={setSearch}>
|
<Page onAdd={onAdd} term={term} search={search}>
|
||||||
{plans?.length === 0 ? (
|
{plans?.length === 0 ? (
|
||||||
<List.Item
|
<List.Item
|
||||||
title="No plans yet"
|
title="No plans yet"
|
||||||
|
@ -61,5 +70,5 @@ export default function PlanList() {
|
||||||
)}
|
)}
|
||||||
</Page>
|
</Page>
|
||||||
</>
|
</>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
17
PlanPage.tsx
|
@ -1,11 +1,11 @@
|
||||||
import {createStackNavigator} from '@react-navigation/stack';
|
import {createStackNavigator} from '@react-navigation/stack'
|
||||||
import React from 'react';
|
import EditPlan from './EditPlan'
|
||||||
import EditPlan from './EditPlan';
|
import EditSet from './EditSet'
|
||||||
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,6 +14,7 @@ export default function PlanPage() {
|
||||||
<Stack.Screen name="PlanList" component={PlanList} />
|
<Stack.Screen name="PlanList" component={PlanList} />
|
||||||
<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.Screen name="EditSet" component={EditSet} />
|
||||||
</Stack.Navigator>
|
</Stack.Navigator>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
39
Routes.tsx
|
@ -1,31 +1,30 @@
|
||||||
import {createDrawerNavigator} from '@react-navigation/drawer';
|
import {createDrawerNavigator} from '@react-navigation/drawer'
|
||||||
import {useNavigation} from '@react-navigation/native';
|
import {useMemo} from 'react'
|
||||||
import React from 'react';
|
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 TimerPage from './TimerPage';
|
import WorkoutsPage from './WorkoutsPage'
|
||||||
import useDark from './use-dark';
|
|
||||||
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 navigation = useNavigation();
|
|
||||||
|
|
||||||
const routes: Route[] = [
|
const routes: Route[] = useMemo(
|
||||||
|
() => [
|
||||||
{name: 'Home', component: HomePage, icon: 'home'},
|
{name: 'Home', component: HomePage, icon: 'home'},
|
||||||
{name: 'Plans', component: PlanPage, icon: 'event'},
|
{name: 'Plans', component: PlanPage, icon: 'event'},
|
||||||
{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: 'Timer', component: TimerPage, icon: 'access-time'},
|
|
||||||
{name: 'Settings', component: SettingsPage, icon: 'settings'},
|
{name: 'Settings', component: SettingsPage, icon: 'settings'},
|
||||||
];
|
],
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Drawer.Navigator
|
<Drawer.Navigator
|
||||||
|
@ -45,5 +44,5 @@ export default function Routes() {
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Drawer.Navigator>
|
</Drawer.Navigator>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
24
Select.tsx
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import {Picker} from '@react-native-picker/picker'
|
||||||
|
import {useTheme} from 'react-native-paper'
|
||||||
|
|
||||||
|
export default function Select({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
value: string
|
||||||
|
onChange: (value: string) => void
|
||||||
|
children: JSX.Element | JSX.Element[]
|
||||||
|
}) {
|
||||||
|
const {colors} = useTheme()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Picker
|
||||||
|
style={{color: colors.primary, marginTop: -10}}
|
||||||
|
dropdownIconColor={colors.text}
|
||||||
|
selectedValue={value}
|
||||||
|
onValueChange={onChange}>
|
||||||
|
{children}
|
||||||
|
</Picker>
|
||||||
|
)
|
||||||
|
}
|
115
SetForm.tsx
|
@ -1,50 +1,52 @@
|
||||||
import React, {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 MassiveInput from './MassiveInput';
|
import {getNow, setRepo} from './db'
|
||||||
import {useSnackbar} from './MassiveSnack';
|
import GymSet from './gym-set'
|
||||||
import Set from './set';
|
import MassiveInput from './MassiveInput'
|
||||||
import {getSets} from './set.service';
|
import Settings from './settings'
|
||||||
import {useSettings} from './use-settings';
|
import {format} from './time'
|
||||||
|
import {toast} from './toast'
|
||||||
|
|
||||||
export default function SetForm({
|
export default function SetForm({
|
||||||
save,
|
save,
|
||||||
set,
|
set,
|
||||||
|
settings,
|
||||||
}: {
|
}: {
|
||||||
set: Set;
|
set: GymSet
|
||||||
save: (set: Set) => void;
|
save: (set: GymSet) => void
|
||||||
|
settings: Settings
|
||||||
}) {
|
}) {
|
||||||
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 weightRef = useRef<TextInput>(null)
|
||||||
const {settings} = useSettings();
|
const repsRef = useRef<TextInput>(null)
|
||||||
const weightRef = useRef<TextInput>(null);
|
const unitRef = useRef<TextInput>(null)
|
||||||
const repsRef = 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 getSets({search: name, limit: 1, offset: 0}).then(
|
image = await setRepo.findOne({where: {name}}).then(s => s?.image)
|
||||||
([gotSet]) => gotSet?.image,
|
|
||||||
);
|
console.log(`${SetForm.name}.handleSubmit:`, {image})
|
||||||
console.log(`${SetForm.name}.handleSubmit:`, {image});
|
const [{now}] = await getNow()
|
||||||
save({
|
save({
|
||||||
name,
|
name,
|
||||||
|
created: now,
|
||||||
reps: Number(reps),
|
reps: Number(reps),
|
||||||
weight: Number(weight),
|
weight: Number(weight),
|
||||||
id: set.id,
|
id: set.id,
|
||||||
|
@ -53,34 +55,35 @@ export default function SetForm({
|
||||||
minutes: Number(set.minutes ?? 3),
|
minutes: Number(set.minutes ?? 3),
|
||||||
seconds: Number(set.seconds ?? 30),
|
seconds: Number(set.seconds ?? 30),
|
||||||
sets: set.sets ?? 3,
|
sets: set.sets ?? 3,
|
||||||
});
|
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')
|
||||||
};
|
}
|
||||||
|
|
||||||
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')
|
||||||
};
|
}
|
||||||
|
|
||||||
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 (
|
||||||
<>
|
<>
|
||||||
|
@ -112,7 +115,7 @@ export default function SetForm({
|
||||||
onSubmitEditing={handleSubmit}
|
onSubmitEditing={handleSubmit}
|
||||||
innerRef={weightRef}
|
innerRef={weightRef}
|
||||||
/>
|
/>
|
||||||
{!!settings.showUnit && (
|
{settings.showUnit && (
|
||||||
<MassiveInput
|
<MassiveInput
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
label="Unit"
|
label="Unit"
|
||||||
|
@ -121,10 +124,14 @@ export default function SetForm({
|
||||||
innerRef={unitRef}
|
innerRef={unitRef}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{typeof set.id === 'number' && !!settings.showDate && (
|
{typeof set.id === 'number' && settings.showDate && (
|
||||||
<MassiveInput label="Created" disabled value={set.created} />
|
<MassiveInput
|
||||||
|
label="Created"
|
||||||
|
disabled
|
||||||
|
value={format(set.created, settings.date)}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
{!!settings.images && newImage && (
|
{settings.images && newImage && (
|
||||||
<TouchableRipple
|
<TouchableRipple
|
||||||
style={{marginBottom: MARGIN}}
|
style={{marginBottom: MARGIN}}
|
||||||
onPress={changeImage}
|
onPress={changeImage}
|
||||||
|
@ -132,7 +139,7 @@ export default function SetForm({
|
||||||
<Card.Cover source={{uri: newImage}} />
|
<Card.Cover source={{uri: newImage}} />
|
||||||
</TouchableRipple>
|
</TouchableRipple>
|
||||||
)}
|
)}
|
||||||
{!!settings.images && !newImage && (
|
{settings.images && !newImage && (
|
||||||
<Button
|
<Button
|
||||||
style={{marginBottom: MARGIN}}
|
style={{marginBottom: MARGIN}}
|
||||||
onPress={changeImage}
|
onPress={changeImage}
|
||||||
|
@ -156,5 +163,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>
|
||||||
</>
|
</>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
65
SetItem.tsx
|
@ -1,47 +1,48 @@
|
||||||
import {NavigationProp, useNavigation} from '@react-navigation/native';
|
import {NavigationProp, useNavigation} from '@react-navigation/native'
|
||||||
import React, {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 {HomePageParams} from './home-page-params';
|
import {setRepo} from './db'
|
||||||
import Set from './set';
|
import GymSet from './gym-set'
|
||||||
import {deleteSet} from './set.service';
|
import {HomePageParams} from './home-page-params'
|
||||||
import {format} from './time';
|
import Settings from './settings'
|
||||||
import useDark from './use-dark';
|
import {format} from './time'
|
||||||
import {useSettings} from './use-settings';
|
import useDark from './use-dark'
|
||||||
|
|
||||||
export default function SetItem({
|
export default function SetItem({
|
||||||
item,
|
item,
|
||||||
onRemove,
|
onRemove,
|
||||||
|
settings,
|
||||||
}: {
|
}: {
|
||||||
item: Set;
|
item: GymSet
|
||||||
onRemove: () => void;
|
onRemove: () => void
|
||||||
|
settings: Settings
|
||||||
}) {
|
}) {
|
||||||
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 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 deleteSet(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: Set = {...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 (
|
||||||
<>
|
<>
|
||||||
|
@ -51,14 +52,14 @@ export default function SetItem({
|
||||||
description={`${item.reps} x ${item.weight}${item.unit || 'kg'}`}
|
description={`${item.reps} x ${item.weight}${item.unit || 'kg'}`}
|
||||||
onLongPress={longPress}
|
onLongPress={longPress}
|
||||||
left={() =>
|
left={() =>
|
||||||
!!settings.images &&
|
settings.images &&
|
||||||
item.image && (
|
item.image && (
|
||||||
<Image source={{uri: item.image}} style={{height: 75, width: 75}} />
|
<Image source={{uri: item.image}} style={{height: 75, width: 75}} />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
right={() => (
|
right={() => (
|
||||||
<>
|
<>
|
||||||
{!!settings.showDate && (
|
{settings.showDate && (
|
||||||
<Text
|
<Text
|
||||||
style={{
|
style={{
|
||||||
alignSelf: 'center',
|
alignSelf: 'center',
|
||||||
|
@ -79,5 +80,5 @@ export default function SetItem({
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
149
SetList.tsx
|
@ -2,105 +2,118 @@ import {
|
||||||
NavigationProp,
|
NavigationProp,
|
||||||
useFocusEffect,
|
useFocusEffect,
|
||||||
useNavigation,
|
useNavigation,
|
||||||
} from '@react-navigation/native';
|
} from '@react-navigation/native'
|
||||||
import React, {useCallback, useEffect, useState} from 'react';
|
import {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 DrawerHeader from './DrawerHeader';
|
import {Like} from 'typeorm'
|
||||||
import {HomePageParams} from './home-page-params';
|
import {getNow, setRepo, settingsRepo} from './db'
|
||||||
import Page from './Page';
|
import DrawerHeader from './DrawerHeader'
|
||||||
import Set from './set';
|
import GymSet from './gym-set'
|
||||||
import {defaultSet, getSets, getToday} from './set.service';
|
import {HomePageParams} from './home-page-params'
|
||||||
import SetItem from './SetItem';
|
import Page from './Page'
|
||||||
import {useSettings} from './use-settings';
|
import SetItem from './SetItem'
|
||||||
|
import Settings from './settings'
|
||||||
|
|
||||||
const limit = 15;
|
const limit = 15
|
||||||
|
|
||||||
export default function SetList() {
|
export default function SetList() {
|
||||||
const [sets, setSets] = useState<Set[]>();
|
const [sets, setSets] = useState<GymSet[]>([])
|
||||||
const [set, setSet] = useState<Set>();
|
const [set, setSet] = useState<GymSet>()
|
||||||
const [offset, setOffset] = useState(0);
|
const [offset, setOffset] = useState(0)
|
||||||
const [search, setSearch] = useState('');
|
const [term, setTerm] = useState('')
|
||||||
const [end, setEnd] = useState(false);
|
const [end, setEnd] = useState(false)
|
||||||
const {settings} = useSettings();
|
const [settings, setSettings] = useState<Settings>()
|
||||||
const navigation = useNavigation<NavigationProp<HomePageParams>>();
|
const navigation = useNavigation<NavigationProp<HomePageParams>>()
|
||||||
|
|
||||||
const refresh = useCallback(async () => {
|
const refresh = useCallback(async (value: string) => {
|
||||||
const todaysSet = await getToday();
|
const newSets = await setRepo.find({
|
||||||
if (todaysSet) setSet({...todaysSet});
|
where: {name: Like(`%${value}%`), hidden: 0 as any},
|
||||||
const newSets = await getSets({
|
take: limit,
|
||||||
search: `%${search}%`,
|
skip: 0,
|
||||||
limit,
|
order: {created: 'DESC'},
|
||||||
offset: 0,
|
})
|
||||||
format: settings.date || '%Y-%m-%d %H:%M',
|
console.log(`${SetList.name}.refresh:`, {newSets})
|
||||||
});
|
setSet(newSets[0])
|
||||||
console.log(`${SetList.name}.refresh:`, {first: 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);
|
}, [])
|
||||||
}, [search, settings.date]);
|
|
||||||
|
|
||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
refresh();
|
refresh(term)
|
||||||
}, [refresh]),
|
settingsRepo.findOne({where: {}}).then(setSettings)
|
||||||
);
|
}, [refresh, term]),
|
||||||
|
)
|
||||||
useEffect(() => {
|
|
||||||
refresh();
|
|
||||||
}, [search, refresh]);
|
|
||||||
|
|
||||||
const renderItem = useCallback(
|
const renderItem = useCallback(
|
||||||
({item}: {item: Set}) => (
|
({item}: {item: GymSet}) => (
|
||||||
<SetItem item={item} key={item.id} onRemove={refresh} />
|
<SetItem
|
||||||
|
settings={settings}
|
||||||
|
item={item}
|
||||||
|
key={item.id}
|
||||||
|
onRemove={() => refresh(term)}
|
||||||
|
/>
|
||||||
),
|
),
|
||||||
[refresh],
|
[refresh, term, settings],
|
||||||
);
|
)
|
||||||
|
|
||||||
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, search});
|
console.log(`${SetList.name}.next:`, {offset, newOffset, term})
|
||||||
const newSets = await getSets({
|
const newSets = await setRepo.find({
|
||||||
search: `%${search}%`,
|
where: {name: Like(`%${term}%`), hidden: 0 as any},
|
||||||
limit,
|
take: limit,
|
||||||
offset: newOffset,
|
skip: newOffset,
|
||||||
format: settings.date || '%Y-%m-%d %H:%M',
|
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)
|
||||||
}, [search, end, offset, sets, settings.date]);
|
}, [term, end, offset, sets])
|
||||||
|
|
||||||
const onAdd = useCallback(async () => {
|
const onAdd = useCallback(async () => {
|
||||||
console.log(`${SetList.name}.onAdd`, {set});
|
console.log(`${SetList.name}.onAdd`, {set})
|
||||||
navigation.navigate('EditSet', {
|
const [{now}] = await getNow()
|
||||||
set: set || {...defaultSet},
|
const newSet: GymSet = set || new GymSet()
|
||||||
});
|
delete newSet.id
|
||||||
}, [navigation, set]);
|
newSet.created = now
|
||||||
|
navigation.navigate('EditSet', {set: newSet})
|
||||||
|
}, [navigation, set])
|
||||||
|
|
||||||
|
const search = useCallback(
|
||||||
|
(value: string) => {
|
||||||
|
setTerm(value)
|
||||||
|
refresh(value)
|
||||||
|
},
|
||||||
|
[refresh],
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DrawerHeader name="Home" />
|
<DrawerHeader name="Home" />
|
||||||
<Page onAdd={onAdd} search={search} setSearch={setSearch}>
|
<Page onAdd={onAdd} term={term} search={search}>
|
||||||
{sets?.length === 0 ? (
|
{sets?.length === 0 ? (
|
||||||
<List.Item
|
<List.Item
|
||||||
title="No sets yet"
|
title="No sets yet"
|
||||||
description="A set is a group of repetitions. E.g. 8 reps of Squats."
|
description="A set is a group of repetitions. E.g. 8 reps of Squats."
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
settings && (
|
||||||
<FlatList
|
<FlatList
|
||||||
data={sets}
|
data={sets}
|
||||||
style={{flex: 1}}
|
style={{flex: 1}}
|
||||||
renderItem={renderItem}
|
renderItem={renderItem}
|
||||||
keyExtractor={s => s.id!.toString()}
|
|
||||||
onEndReached={next}
|
onEndReached={next}
|
||||||
/>
|
/>
|
||||||
|
)
|
||||||
)}
|
)}
|
||||||
</Page>
|
</Page>
|
||||||
</>
|
</>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
341
SettingsPage.tsx
|
@ -1,190 +1,185 @@
|
||||||
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 React, {useCallback, useEffect, useState} from 'react';
|
import {useCallback, useMemo, useState} from 'react'
|
||||||
import {NativeModules, ScrollView} from 'react-native';
|
import {DeviceEventEmitter, NativeModules, ScrollView, View} 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 {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 DrawerHeader from './DrawerHeader';
|
import DrawerHeader from './DrawerHeader'
|
||||||
import Input from './input';
|
import Input from './input'
|
||||||
import {useSnackbar} from './MassiveSnack';
|
import Page from './Page'
|
||||||
import Page from './Page';
|
import Select from './Select'
|
||||||
import Settings from './settings';
|
import Switch from './Switch'
|
||||||
import {updateSettings} from './settings.service';
|
import {toast} from './toast'
|
||||||
import Switch from './Switch';
|
import {useTheme} from './use-theme'
|
||||||
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 [search, setSearch] = useState('');
|
const [term, setTerm] = useState('')
|
||||||
const {settings, setSettings} = useSettings();
|
const [vibrate, setVibrate] = useState(false)
|
||||||
const {
|
const [alarm, setAlarm] = useState(false)
|
||||||
vibrate,
|
const [sound, setSound] = useState('')
|
||||||
sound,
|
const [notify, setNotify] = useState(false)
|
||||||
notify,
|
const [images, setImages] = useState(false)
|
||||||
images,
|
const [showUnit, setShowUnit] = useState(false)
|
||||||
showUnit,
|
const [steps, setSteps] = useState(false)
|
||||||
steps,
|
const [date, setDate] = useState('%Y-%m-%d %H:%M')
|
||||||
showDate,
|
const {theme, setTheme, color, setColor} = useTheme()
|
||||||
showSets,
|
const [showDate, setShowDate] = useState(false)
|
||||||
theme,
|
const [showSets, setShowSets] = useState(false)
|
||||||
alarm,
|
const [noSound, setNoSound] = useState(false)
|
||||||
noSound,
|
|
||||||
} = settings;
|
|
||||||
const {color, setColor} = useColor();
|
|
||||||
const {toast} = useSnackbar();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
console.log(`${SettingsPage.name}.useEffect:`, {settings});
|
|
||||||
}, [settings]);
|
|
||||||
|
|
||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
NativeModules.AlarmModule.ignoringBattery(setIgnoring);
|
NativeModules.AlarmModule.ignoringBattery(setIgnoring)
|
||||||
|
settingsRepo.findOne({where: {}}).then(settings => {
|
||||||
|
setAlarm(settings.alarm)
|
||||||
|
setVibrate(settings.vibrate)
|
||||||
|
setSound(settings.sound)
|
||||||
|
setNotify(settings.notify)
|
||||||
|
setImages(settings.images)
|
||||||
|
setShowUnit(settings.showUnit)
|
||||||
|
setSteps(settings.steps)
|
||||||
|
setDate(settings.date)
|
||||||
|
setShowDate(settings.showDate)
|
||||||
|
setShowSets(settings.showSets)
|
||||||
|
})
|
||||||
}, []),
|
}, []),
|
||||||
);
|
)
|
||||||
|
|
||||||
const update = useCallback(
|
|
||||||
(value: boolean, field: keyof Settings) => {
|
|
||||||
updateSettings({...settings, [field]: +value});
|
|
||||||
setSettings({...settings, [field]: +value});
|
|
||||||
},
|
|
||||||
[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)
|
||||||
else toast('Stopped timers running after each set.', 4000);
|
DeviceEventEmitter.emit('toast', {
|
||||||
if (enabled && !ignoring) setBattery(true);
|
value: 'Timers will now run after each set',
|
||||||
update(enabled, 'alarm');
|
timeout: 4000,
|
||||||
|
})
|
||||||
|
else toast('Stopped timers running after each set.')
|
||||||
|
if (enabled && !ignoring) setBattery(true)
|
||||||
|
setAlarm(enabled)
|
||||||
|
settingsRepo.update({}, {alarm: enabled})
|
||||||
},
|
},
|
||||||
[setBattery, ignoring, toast, update],
|
[setBattery, ignoring],
|
||||||
);
|
)
|
||||||
|
|
||||||
const changeVibrate = useCallback(
|
const changeVibrate = useCallback((enabled: boolean) => {
|
||||||
(enabled: boolean) => {
|
if (enabled) toast('When a timer completes, vibrate your phone.')
|
||||||
if (enabled) toast('When a timer completes, vibrate your phone.', 4000);
|
else toast('Stop vibrating at the end of timers.')
|
||||||
else toast('Stop vibrating at the end of timers.', 4000);
|
setVibrate(enabled)
|
||||||
update(enabled, 'vibrate');
|
settingsRepo.update({}, {vibrate: enabled})
|
||||||
},
|
}, [])
|
||||||
[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
|
||||||
updateSettings({sound: fileCopyUri} as Settings);
|
settingsRepo.update({}, {sound: fileCopyUri})
|
||||||
setSettings({...settings, sound: fileCopyUri});
|
setSound(fileCopyUri)
|
||||||
toast('This song will now play after rest timers complete.', 4000);
|
toast('This song will now play after rest timers complete.')
|
||||||
}, [toast, setSettings, settings]);
|
}, [])
|
||||||
|
|
||||||
const changeNotify = useCallback(
|
const changeNotify = useCallback((enabled: boolean) => {
|
||||||
(enabled: boolean) => {
|
setNotify(enabled)
|
||||||
update(enabled, 'notify');
|
settingsRepo.update({}, {notify: enabled})
|
||||||
if (enabled) toast('Show when a set is a new record.', 4000);
|
if (enabled) toast('Show when a set is a new record.')
|
||||||
else toast('Stopped showing notifications for new records.', 4000);
|
else toast('Stopped showing notifications for new records.')
|
||||||
},
|
}, [])
|
||||||
[toast, update],
|
|
||||||
);
|
|
||||||
|
|
||||||
const changeImages = useCallback(
|
const changeImages = useCallback((enabled: boolean) => {
|
||||||
(enabled: boolean) => {
|
setImages(enabled)
|
||||||
update(enabled, 'images');
|
settingsRepo.update({}, {images: enabled})
|
||||||
if (enabled) toast('Show images for sets.', 4000);
|
if (enabled) toast('Show images for sets.')
|
||||||
else toast('Stopped showing images for sets.', 4000);
|
else toast('Stopped showing images for sets.')
|
||||||
},
|
}, [])
|
||||||
[toast, update],
|
|
||||||
);
|
|
||||||
|
|
||||||
const changeUnit = useCallback(
|
const changeUnit = useCallback((enabled: boolean) => {
|
||||||
(enabled: boolean) => {
|
setShowUnit(enabled)
|
||||||
update(enabled, 'showUnit');
|
settingsRepo.update({}, {showUnit: enabled})
|
||||||
if (enabled) toast('Show option to select unit for sets.', 4000);
|
if (enabled) toast('Show option to select unit for sets.')
|
||||||
else toast('Hid unit option for sets.', 4000);
|
else toast('Hid unit option for sets.')
|
||||||
},
|
}, [])
|
||||||
[toast, update],
|
|
||||||
);
|
|
||||||
|
|
||||||
const changeSteps = useCallback(
|
const changeSteps = useCallback((enabled: boolean) => {
|
||||||
(enabled: boolean) => {
|
setSteps(enabled)
|
||||||
update(enabled, 'steps');
|
settingsRepo.update({}, {steps: enabled})
|
||||||
if (enabled) toast('Show steps for a workout.', 4000);
|
if (enabled) toast('Show steps for a workout.')
|
||||||
else toast('Stopped showing steps for workouts.', 4000);
|
else toast('Stopped showing steps for workouts.')
|
||||||
},
|
}, [])
|
||||||
[toast, update],
|
|
||||||
);
|
|
||||||
|
|
||||||
const changeShowDate = useCallback(
|
const changeShowDate = useCallback((enabled: boolean) => {
|
||||||
(enabled: boolean) => {
|
setShowDate(enabled)
|
||||||
update(enabled, 'showDate');
|
settingsRepo.update({}, {showDate: enabled})
|
||||||
if (enabled) toast('Show date for sets by default.', 4000);
|
if (enabled) toast('Show date for sets by default.')
|
||||||
else toast('Stopped showing date for sets by default.', 4000);
|
else toast('Stopped showing date for sets by default.')
|
||||||
},
|
}, [])
|
||||||
[toast, update],
|
|
||||||
);
|
|
||||||
|
|
||||||
const changeShowSets = useCallback(
|
const changeShowSets = useCallback((enabled: boolean) => {
|
||||||
(enabled: boolean) => {
|
setShowSets(enabled)
|
||||||
update(enabled, 'showSets');
|
settingsRepo.update({}, {showSets: enabled})
|
||||||
if (enabled) toast('Show maximum sets for workouts.', 4000);
|
if (enabled) toast('Show target sets for workouts.')
|
||||||
else toast('Stopped showing maximum sets for workouts.', 4000);
|
else toast('Stopped showing target sets for workouts.')
|
||||||
},
|
}, [])
|
||||||
[toast, update],
|
|
||||||
);
|
|
||||||
|
|
||||||
const changeNoSound = useCallback(
|
const changeNoSound = useCallback((enabled: boolean) => {
|
||||||
(enabled: boolean) => {
|
setNoSound(enabled)
|
||||||
update(enabled, 'noSound');
|
settingsRepo.update({}, {noSound: enabled})
|
||||||
if (enabled) toast('Disable sound on rest timer alarms.', 4000);
|
if (enabled) toast('Disable sound on rest timer alarms.')
|
||||||
else toast('Enabled sound for rest timer alarms.', 4000);
|
else toast('Enabled sound for rest timer alarms.')
|
||||||
},
|
}, [])
|
||||||
[toast, update],
|
|
||||||
);
|
|
||||||
|
|
||||||
const switches: Input<boolean>[] = [
|
const switches: Input<boolean>[] = [
|
||||||
{name: 'Rest timers', value: !!alarm, onChange: changeAlarmEnabled},
|
{name: 'Rest timers', value: alarm, onChange: changeAlarmEnabled},
|
||||||
{name: 'Vibrate', value: !!vibrate, onChange: changeVibrate},
|
{name: 'Vibrate', value: vibrate, onChange: changeVibrate},
|
||||||
{name: 'Disable sound', value: !!noSound, onChange: changeNoSound},
|
{name: 'Disable sound', value: noSound, onChange: changeNoSound},
|
||||||
{name: 'Record notifications', value: !!notify, onChange: changeNotify},
|
{name: 'Notifications', value: notify, onChange: changeNotify},
|
||||||
{name: 'Show images', value: !!images, onChange: changeImages},
|
{name: 'Show images', value: images, onChange: changeImages},
|
||||||
{name: 'Show unit', value: !!showUnit, onChange: changeUnit},
|
{name: 'Show unit', value: showUnit, onChange: changeUnit},
|
||||||
{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) => {
|
||||||
updateSettings({...settings, theme: value as any});
|
settingsRepo.update({}, {theme: value})
|
||||||
setSettings({...settings, theme: value as any});
|
setTheme(value)
|
||||||
},
|
},
|
||||||
[settings, setSettings],
|
[setTheme],
|
||||||
);
|
)
|
||||||
|
|
||||||
const changeDate = useCallback(
|
const changeDate = useCallback((value: string) => {
|
||||||
|
settingsRepo.update({}, {date: value})
|
||||||
|
setDate(value)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const soundString = useMemo(() => {
|
||||||
|
if (!sound) return null
|
||||||
|
const split = sound.split('/')
|
||||||
|
return ': ' + split.pop()
|
||||||
|
}, [sound])
|
||||||
|
|
||||||
|
const changeColor = useCallback(
|
||||||
(value: string) => {
|
(value: string) => {
|
||||||
updateSettings({...settings, date: value as any});
|
setColor(value)
|
||||||
setSettings({...settings, date: value as any});
|
settingsRepo.update({}, {color: value})
|
||||||
},
|
},
|
||||||
[settings, setSettings],
|
[setColor],
|
||||||
);
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DrawerHeader name="Settings" />
|
<DrawerHeader name="Settings" />
|
||||||
<Page search={search} setSearch={setSearch}>
|
<Page term={term} search={setTerm}>
|
||||||
<ScrollView style={{marginTop: MARGIN}}>
|
<ScrollView style={{marginTop: MARGIN}}>
|
||||||
{switches
|
{switches
|
||||||
.filter(input =>
|
.filter(input =>
|
||||||
input.name.toLowerCase().includes(search.toLowerCase()),
|
input.name.toLowerCase().includes(term.toLowerCase()),
|
||||||
)
|
)
|
||||||
.map(input => (
|
.map(input => (
|
||||||
<Switch
|
<Switch
|
||||||
|
@ -195,39 +190,28 @@ export default function SettingsPage() {
|
||||||
{input.name}
|
{input.name}
|
||||||
</Switch>
|
</Switch>
|
||||||
))}
|
))}
|
||||||
{'theme'.includes(search.toLowerCase()) && (
|
<View style={{marginBottom: 10}} />
|
||||||
<Picker
|
{'theme'.includes(term.toLowerCase()) && (
|
||||||
style={{color}}
|
<Select value={theme} onChange={changeTheme}>
|
||||||
dropdownIconColor={color}
|
|
||||||
selectedValue={theme}
|
|
||||||
onValueChange={changeTheme}>
|
|
||||||
<Picker.Item value="system" label="Follow system theme" />
|
<Picker.Item value="system" label="Follow system theme" />
|
||||||
<Picker.Item value="dark" label="Dark theme" />
|
<Picker.Item value="dark" label="Dark theme" />
|
||||||
<Picker.Item value="light" label="Light theme" />
|
<Picker.Item value="light" label="Light theme" />
|
||||||
</Picker>
|
</Select>
|
||||||
)}
|
)}
|
||||||
{'color'.includes(search.toLowerCase()) && (
|
{'color'.includes(term.toLowerCase()) && (
|
||||||
<Picker
|
<Select value={color} onChange={changeColor}>
|
||||||
style={{color, marginTop: -10}}
|
|
||||||
dropdownIconColor={color}
|
|
||||||
selectedValue={color}
|
|
||||||
onValueChange={value => setColor(value)}>
|
|
||||||
{lightColors.concat(darkColors).map(colorOption => (
|
{lightColors.concat(darkColors).map(colorOption => (
|
||||||
<Picker.Item
|
<Picker.Item
|
||||||
key={colorOption.hex}
|
key={colorOption}
|
||||||
value={colorOption.hex}
|
value={colorOption}
|
||||||
label="Primary color"
|
label="Primary color"
|
||||||
color={colorOption.hex}
|
color={colorOption}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Picker>
|
</Select>
|
||||||
)}
|
)}
|
||||||
{'date format'.includes(search.toLowerCase()) && (
|
{'date format'.includes(term.toLowerCase()) && (
|
||||||
<Picker
|
<Select value={date} onChange={changeDate}>
|
||||||
style={{color, marginTop: -10}}
|
|
||||||
dropdownIconColor={color}
|
|
||||||
selectedValue={settings.date}
|
|
||||||
onValueChange={changeDate}>
|
|
||||||
<Picker.Item value="%Y-%m-%d %H:%M" label="1990-12-24 15:05" />
|
<Picker.Item value="%Y-%m-%d %H:%M" label="1990-12-24 15:05" />
|
||||||
<Picker.Item value="%Y-%m-%d" label="1990-12-24" />
|
<Picker.Item value="%Y-%m-%d" label="1990-12-24" />
|
||||||
<Picker.Item value="%d/%m" label="24/12 (dd/MM)" />
|
<Picker.Item value="%d/%m" label="24/12 (dd/MM)" />
|
||||||
|
@ -240,14 +224,11 @@ export default function SettingsPage() {
|
||||||
label="24/12/1990 3:05 PM"
|
label="24/12/1990 3:05 PM"
|
||||||
/>
|
/>
|
||||||
<Picker.Item value="%d/%m %h:%M %p" label="24/12 3:05 PM" />
|
<Picker.Item value="%d/%m %h:%M %p" label="24/12 3:05 PM" />
|
||||||
</Picker>
|
</Select>
|
||||||
)}
|
)}
|
||||||
{'alarm sound'.includes(search.toLowerCase()) && (
|
{'alarm sound'.includes(term.toLowerCase()) && (
|
||||||
<Button style={{alignSelf: 'flex-start'}} onPress={changeSound}>
|
<Button style={{alignSelf: 'flex-start'}} onPress={changeSound}>
|
||||||
Alarm sound
|
Alarm sound{soundString}
|
||||||
{sound
|
|
||||||
? ': ' + sound.split('/')[sound.split('/').length - 1]
|
|
||||||
: null}
|
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
@ -256,12 +237,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>
|
||||||
</>
|
</>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,30 +1,36 @@
|
||||||
import {useNavigation} from '@react-navigation/native';
|
import {useNavigation} from '@react-navigation/native'
|
||||||
import React from 'react';
|
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'
|
||||||
|
|
||||||
export default function StackHeader({title}: {title: string}) {
|
export default function StackHeader({title}: {title: string}) {
|
||||||
const navigation = useNavigation();
|
const navigation = useNavigation()
|
||||||
|
const dark = useDark()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Appbar.Header>
|
<Appbar.Header>
|
||||||
<IconButton icon="arrow-back" onPress={navigation.goBack} />
|
<IconButton
|
||||||
|
color={dark ? 'white' : 'white'}
|
||||||
|
icon="arrow-back"
|
||||||
|
onPress={navigation.goBack}
|
||||||
|
/>
|
||||||
<Appbar.Content title={title} />
|
<Appbar.Content title={title} />
|
||||||
<IconButton
|
<IconButton
|
||||||
|
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>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
218
StartPlan.tsx
|
@ -1,108 +1,120 @@
|
||||||
import {RouteProp, useFocusEffect, useRoute} from '@react-navigation/native';
|
import {RouteProp, useRoute} from '@react-navigation/native'
|
||||||
import React, {useCallback, useMemo, useRef, useState} from 'react';
|
import {useCallback, useEffect, 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, List, RadioButton} from 'react-native-paper';
|
import {Button} from 'react-native-paper'
|
||||||
import {getBestSet} from './best.service';
|
import {getBestSet} from './best.service'
|
||||||
import {useColor} from './color';
|
import {PADDING} from './constants'
|
||||||
import {PADDING} from './constants';
|
import CountMany from './count-many'
|
||||||
import CountMany from './count-many';
|
import {AppDataSource} from './data-source'
|
||||||
import MassiveInput from './MassiveInput';
|
import {getNow, setRepo, settingsRepo} from './db'
|
||||||
import {useSnackbar} from './MassiveSnack';
|
import GymSet from './gym-set'
|
||||||
import {PlanPageParams} from './plan-page-params';
|
import MassiveInput from './MassiveInput'
|
||||||
import Set from './set';
|
import {PlanPageParams} from './plan-page-params'
|
||||||
import {addSet, countMany} from './set.service';
|
import SetForm from './SetForm'
|
||||||
import SetForm from './SetForm';
|
import Settings from './settings'
|
||||||
import StackHeader from './StackHeader';
|
import StackHeader from './StackHeader'
|
||||||
import {useSettings} from './use-settings';
|
import StartPlanItem from './StartPlanItem'
|
||||||
|
import {toast} from './toast'
|
||||||
|
|
||||||
export default function StartPlan() {
|
export default function StartPlan() {
|
||||||
const {params} = useRoute<RouteProp<PlanPageParams, 'StartPlan'>>();
|
const {params} = useRoute<RouteProp<PlanPageParams, 'StartPlan'>>()
|
||||||
const {set} = params;
|
const [reps, setReps] = useState('')
|
||||||
const [name, setName] = useState(set.name);
|
const [weight, setWeight] = useState('')
|
||||||
const [reps, setReps] = useState(set.reps.toString());
|
const [unit, setUnit] = useState<string>('kg')
|
||||||
const [weight, setWeight] = useState(set.weight.toString());
|
const [best, setBest] = useState<GymSet>()
|
||||||
const [unit, setUnit] = useState<string>();
|
const [selected, setSelected] = useState(0)
|
||||||
const {toast} = useSnackbar();
|
const [settings, setSettings] = useState<Settings>()
|
||||||
const [minutes, setMinutes] = useState(set.minutes);
|
const [counts, setCounts] = useState<CountMany[]>()
|
||||||
const [seconds, setSeconds] = useState(set.seconds);
|
const weightRef = useRef<TextInput>(null)
|
||||||
const [best, setBest] = useState<Set>(set);
|
const repsRef = useRef<TextInput>(null)
|
||||||
const [selected, setSelected] = useState(0);
|
const unitRef = useRef<TextInput>(null)
|
||||||
const {settings} = useSettings();
|
const workouts = useMemo(() => params.plan.workouts.split(','), [params])
|
||||||
const [counts, setCounts] = useState<CountMany[]>();
|
|
||||||
const weightRef = useRef<TextInput>(null);
|
|
||||||
const repsRef = useRef<TextInput>(null);
|
|
||||||
const unitRef = useRef<TextInput>(null);
|
|
||||||
const workouts = useMemo(() => params.plan.workouts.split(','), [params]);
|
|
||||||
const {color} = useColor();
|
|
||||||
|
|
||||||
const [selection, setSelection] = useState({
|
const [selection, setSelection] = useState({
|
||||||
start: 0,
|
start: 0,
|
||||||
end: set.reps.toString().length,
|
end: 0,
|
||||||
});
|
})
|
||||||
|
|
||||||
useFocusEffect(
|
const refresh = useCallback(() => {
|
||||||
useCallback(() => {
|
const questions = workouts
|
||||||
countMany(workouts).then(newCounts => {
|
.map((workout, index) => `('${workout}',${index})`)
|
||||||
setCounts(newCounts);
|
.join(',')
|
||||||
console.log(`${StartPlan.name}.focus:`, {newCounts});
|
console.log({questions, workouts})
|
||||||
});
|
const select = `
|
||||||
}, [params]),
|
SELECT workouts.name, COUNT(sets.id) as total
|
||||||
);
|
FROM (select 0 as name, 0 as sequence union values ${questions}) as workouts
|
||||||
|
LEFT JOIN sets ON sets.name = workouts.name
|
||||||
|
AND sets.created LIKE STRFTIME('%Y-%m-%d%%', 'now', 'localtime')
|
||||||
|
AND NOT sets.hidden
|
||||||
|
GROUP BY workouts.name
|
||||||
|
ORDER BY workouts.sequence
|
||||||
|
LIMIT -1
|
||||||
|
OFFSET 1
|
||||||
|
`
|
||||||
|
return AppDataSource.manager.query(select).then(newCounts => {
|
||||||
|
setCounts(newCounts)
|
||||||
|
console.log(`${StartPlan.name}.focus:`, {newCounts})
|
||||||
|
return newCounts
|
||||||
|
})
|
||||||
|
}, [workouts])
|
||||||
|
|
||||||
|
const select = useCallback(
|
||||||
|
async (index: number, newCounts?: CountMany[]) => {
|
||||||
|
setSelected(index)
|
||||||
|
console.log(`${StartPlan.name}.next:`, {best, index})
|
||||||
|
if (!counts && !newCounts) return
|
||||||
|
const workout = counts ? counts[index] : newCounts[index]
|
||||||
|
console.log(`${StartPlan.name}.next:`, {workout})
|
||||||
|
const newBest = await getBestSet(workout.name)
|
||||||
|
delete newBest.id
|
||||||
|
console.log(`${StartPlan.name}.next:`, {newBest})
|
||||||
|
setReps(newBest.reps.toString())
|
||||||
|
setWeight(newBest.weight.toString())
|
||||||
|
setUnit(newBest.unit)
|
||||||
|
setBest(newBest)
|
||||||
|
},
|
||||||
|
[counts, best],
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
refresh().then(newCounts => select(0, newCounts))
|
||||||
|
settingsRepo.findOne({where: {}}).then(setSettings)
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [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})
|
||||||
await addSet({
|
const [{now}] = await getNow()
|
||||||
name,
|
await setRepo.save({
|
||||||
|
...best,
|
||||||
weight: +weight,
|
weight: +weight,
|
||||||
reps: +reps,
|
reps: +reps,
|
||||||
minutes: set.minutes,
|
|
||||||
seconds: set.seconds,
|
|
||||||
steps: set.steps,
|
|
||||||
image: set.image,
|
|
||||||
unit,
|
unit,
|
||||||
});
|
created: now,
|
||||||
countMany(workouts).then(setCounts);
|
hidden: false,
|
||||||
|
})
|
||||||
|
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.")
|
||||||
else if (settings.alarm) toast('Resting...', 3000);
|
else if (settings.alarm) toast('Resting...')
|
||||||
else toast('Added set', 3000);
|
else toast('Added set')
|
||||||
if (!settings.alarm) return;
|
if (!settings.alarm) return
|
||||||
const milliseconds = Number(minutes) * 60 * 1000 + Number(seconds) * 1000;
|
const milliseconds =
|
||||||
const args = [milliseconds, !!settings.vibrate, settings.sound];
|
Number(best.minutes) * 60 * 1000 + Number(best.seconds) * 1000
|
||||||
NativeModules.AlarmModule.timer(...args);
|
const {vibrate, sound, noSound} = settings
|
||||||
};
|
const args = [milliseconds, vibrate, sound, noSound]
|
||||||
|
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')
|
||||||
},
|
}, [])
|
||||||
[toast],
|
|
||||||
);
|
|
||||||
|
|
||||||
const select = useCallback(
|
|
||||||
async (index: number) => {
|
|
||||||
setSelected(index);
|
|
||||||
console.log(`${StartPlan.name}.next:`, {name});
|
|
||||||
if (!counts) return;
|
|
||||||
const workout = counts[index];
|
|
||||||
console.log(`${StartPlan.name}.next:`, {workout});
|
|
||||||
const newBest = await getBestSet(workout.name);
|
|
||||||
setMinutes(newBest.minutes);
|
|
||||||
setSeconds(newBest.seconds);
|
|
||||||
setName(newBest.name);
|
|
||||||
setReps(newBest.reps.toString());
|
|
||||||
setWeight(newBest.weight.toString());
|
|
||||||
setUnit(newBest.unit);
|
|
||||||
setBest(newBest);
|
|
||||||
},
|
|
||||||
[name, workouts],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -128,7 +140,7 @@ export default function StartPlan() {
|
||||||
innerRef={weightRef}
|
innerRef={weightRef}
|
||||||
blurOnSubmit
|
blurOnSubmit
|
||||||
/>
|
/>
|
||||||
{!!settings.showUnit && (
|
{settings?.showUnit && (
|
||||||
<MassiveInput
|
<MassiveInput
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
label="Unit"
|
label="Unit"
|
||||||
|
@ -140,26 +152,12 @@ export default function StartPlan() {
|
||||||
{counts && (
|
{counts && (
|
||||||
<FlatList
|
<FlatList
|
||||||
data={counts}
|
data={counts}
|
||||||
renderItem={({item, index}) => (
|
renderItem={props => (
|
||||||
<List.Item
|
<StartPlanItem
|
||||||
title={item.name}
|
{...props}
|
||||||
description={
|
onUndo={refresh}
|
||||||
settings.showSets
|
onSelect={select}
|
||||||
? `${item.total} / ${item.sets ?? 3}`
|
selected={selected}
|
||||||
: item.total.toString()
|
|
||||||
}
|
|
||||||
onPress={() => select(index)}
|
|
||||||
left={() => (
|
|
||||||
<View
|
|
||||||
style={{alignItems: 'center', justifyContent: 'center'}}>
|
|
||||||
<RadioButton
|
|
||||||
onPress={() => select(index)}
|
|
||||||
value={index.toString()}
|
|
||||||
status={selected === index ? 'checked' : 'unchecked'}
|
|
||||||
color={color}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
@ -170,5 +168,5 @@ export default function StartPlan() {
|
||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
</>
|
</>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
94
StartPlanItem.tsx
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
import {NavigationProp, useNavigation} from '@react-navigation/native'
|
||||||
|
import React, {useCallback, useState} from 'react'
|
||||||
|
import {GestureResponderEvent, ListRenderItemInfo, View} from 'react-native'
|
||||||
|
import {List, Menu, RadioButton, useTheme} from 'react-native-paper'
|
||||||
|
import {Like} from 'typeorm'
|
||||||
|
import CountMany from './count-many'
|
||||||
|
import {getNow, setRepo} from './db'
|
||||||
|
import {PlanPageParams} from './plan-page-params'
|
||||||
|
import {toast} from './toast'
|
||||||
|
|
||||||
|
interface Props extends ListRenderItemInfo<CountMany> {
|
||||||
|
onSelect: (index: number) => void
|
||||||
|
selected: number
|
||||||
|
onUndo: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function StartPlanItem(props: Props) {
|
||||||
|
const {index, item, onSelect, selected, onUndo} = props
|
||||||
|
const {colors} = useTheme()
|
||||||
|
const [anchor, setAnchor] = useState({x: 0, y: 0})
|
||||||
|
const [showMenu, setShowMenu] = useState(false)
|
||||||
|
const {navigate} = useNavigation<NavigationProp<PlanPageParams>>()
|
||||||
|
|
||||||
|
const undo = useCallback(async () => {
|
||||||
|
const [{now}] = await getNow()
|
||||||
|
const created = now.split('T')[0]
|
||||||
|
const first = await setRepo.findOne({
|
||||||
|
where: {
|
||||||
|
name: item.name,
|
||||||
|
hidden: 0 as any,
|
||||||
|
created: Like(`${created}%`),
|
||||||
|
},
|
||||||
|
order: {created: 'desc'},
|
||||||
|
})
|
||||||
|
setShowMenu(false)
|
||||||
|
if (!first) return toast('Nothing to undo.')
|
||||||
|
await setRepo.delete(first.id)
|
||||||
|
onUndo()
|
||||||
|
}, [setShowMenu, onUndo, item.name])
|
||||||
|
|
||||||
|
const longPress = useCallback(
|
||||||
|
(e: GestureResponderEvent) => {
|
||||||
|
setAnchor({x: e.nativeEvent.pageX, y: e.nativeEvent.pageY})
|
||||||
|
setShowMenu(true)
|
||||||
|
},
|
||||||
|
[setShowMenu, setAnchor],
|
||||||
|
)
|
||||||
|
|
||||||
|
const edit = async () => {
|
||||||
|
const [{now}] = await getNow()
|
||||||
|
const created = now.split('T')[0]
|
||||||
|
const first = await setRepo.findOne({
|
||||||
|
where: {
|
||||||
|
name: item.name,
|
||||||
|
hidden: 0 as any,
|
||||||
|
created: Like(`${created}%`),
|
||||||
|
},
|
||||||
|
order: {created: 'desc'},
|
||||||
|
})
|
||||||
|
setShowMenu(false)
|
||||||
|
if (!first) return toast('Nothing to edit.')
|
||||||
|
navigate('EditSet', {set: first})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<List.Item
|
||||||
|
onLongPress={longPress}
|
||||||
|
title={item.name}
|
||||||
|
description={item.total.toString()}
|
||||||
|
onPress={() => onSelect(index)}
|
||||||
|
left={() => (
|
||||||
|
<View style={{alignItems: 'center', justifyContent: 'center'}}>
|
||||||
|
<RadioButton
|
||||||
|
onPress={() => onSelect(index)}
|
||||||
|
value={index.toString()}
|
||||||
|
status={selected === index ? 'checked' : 'unchecked'}
|
||||||
|
color={colors.primary}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
right={() => (
|
||||||
|
<>
|
||||||
|
<Menu
|
||||||
|
anchor={anchor}
|
||||||
|
visible={showMenu}
|
||||||
|
onDismiss={() => setShowMenu(false)}>
|
||||||
|
<Menu.Item icon="edit" onPress={edit} title="Edit" />
|
||||||
|
<Menu.Item icon="undo" onPress={undo} title="Undo" />
|
||||||
|
</Menu>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
41
Switch.tsx
|
@ -1,11 +1,10 @@
|
||||||
import React, {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, useTheme} from 'react-native-paper'
|
||||||
import {CombinedDarkTheme, CombinedDefaultTheme} from './App';
|
import {CombinedDarkTheme, CombinedDefaultTheme} from './App'
|
||||||
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 +12,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 {colors} = useTheme()
|
||||||
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(colors.primary, -40),
|
||||||
};
|
}
|
||||||
return {
|
return {
|
||||||
false: CombinedDefaultTheme.colors.placeholder,
|
false: CombinedDefaultTheme.colors.placeholder,
|
||||||
true: colorShade(color, -40),
|
true: colorShade(colors.primary, -40),
|
||||||
};
|
}
|
||||||
}, [dark, color]);
|
}, [dark, colors.primary])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Pressable
|
<Pressable
|
||||||
|
@ -43,12 +42,12 @@ export default function Switch({
|
||||||
}}>
|
}}>
|
||||||
<PaperSwitch
|
<PaperSwitch
|
||||||
trackColor={track}
|
trackColor={track}
|
||||||
color={color}
|
color={colors.primary}
|
||||||
style={{marginRight: MARGIN}}
|
style={{marginRight: MARGIN}}
|
||||||
value={value}
|
value={value}
|
||||||
onValueChange={onValueChange}
|
onValueChange={onValueChange}
|
||||||
/>
|
/>
|
||||||
<Text>{children}</Text>
|
<Text>{children}</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
86
ViewBest.tsx
|
@ -1,41 +1,67 @@
|
||||||
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 React, {useEffect, useState} from 'react';
|
import {useEffect, useState} from 'react'
|
||||||
import {View} from 'react-native';
|
import {View} from 'react-native'
|
||||||
import {getOneRepMax, getVolumes, getWeightsBy} from './best.service';
|
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 {Metrics} from './metrics';
|
import GymSet from './gym-set'
|
||||||
import {Periods} from './periods';
|
import {Metrics} from './metrics'
|
||||||
import Set from './set';
|
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<Set[]>([]);
|
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'
|
||||||
|
if (period === Periods.Monthly) difference = '-1 months'
|
||||||
|
else if (period === Periods.Yearly) difference = '-1 years'
|
||||||
|
let group = '%Y-%m-%d'
|
||||||
|
if (period === Periods.Yearly) group = '%Y-%m'
|
||||||
|
const builder = setRepo
|
||||||
|
.createQueryBuilder()
|
||||||
|
.select("STRFTIME('%Y-%m-%d', created)", 'created')
|
||||||
|
.addSelect('unit')
|
||||||
|
.where('name = :name', {name: params.best.name})
|
||||||
|
.andWhere('NOT hidden')
|
||||||
|
.andWhere("DATE(created) >= DATE('now', 'weekday 0', :difference)", {
|
||||||
|
difference,
|
||||||
|
})
|
||||||
|
.groupBy('name')
|
||||||
|
.addGroupBy(`STRFTIME('${group}', created)`)
|
||||||
switch (metric) {
|
switch (metric) {
|
||||||
case Metrics.Weight:
|
case Metrics.Weight:
|
||||||
getWeightsBy(params.best.name, period).then(setWeights);
|
builder.addSelect('MAX(weight)', 'weight').getRawMany().then(setWeights)
|
||||||
break;
|
break
|
||||||
case Metrics.Volume:
|
case Metrics.Volume:
|
||||||
getVolumes(params.best.name, period).then(setVolumes);
|
builder
|
||||||
break;
|
.addSelect('SUM(weight * reps)', 'value')
|
||||||
|
.getRawMany()
|
||||||
|
.then(setVolumes)
|
||||||
|
break
|
||||||
default:
|
default:
|
||||||
getOneRepMax({name: params.best.name, period}).then(setWeights);
|
// Brzycki formula https://en.wikipedia.org/wiki/One-repetition_maximum#Brzycki
|
||||||
|
builder
|
||||||
|
.addSelect('MAX(weight / (1.0278 - 0.0278 * reps))', 'weight')
|
||||||
|
.getRawMany()
|
||||||
|
.then(newWeights => {
|
||||||
|
console.log({weights: newWeights})
|
||||||
|
setWeights(newWeights)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}, [params.best.name, metric, period]);
|
}, [params.best.name, metric, period])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -80,5 +106,5 @@ export default function ViewBest() {
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
</>
|
</>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,48 +1,44 @@
|
||||||
import {NavigationProp, useNavigation} from '@react-navigation/native';
|
import {NavigationProp, useNavigation} from '@react-navigation/native'
|
||||||
import React, {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 Set from './set';
|
import {setRepo} from './db'
|
||||||
import {deleteSetsBy} from './set.service';
|
import GymSet from './gym-set'
|
||||||
import {useSettings} from './use-settings';
|
import {WorkoutsPageParams} from './WorkoutsPage'
|
||||||
import {WorkoutsPageParams} from './WorkoutsPage';
|
|
||||||
|
|
||||||
export default function WorkoutItem({
|
export default function WorkoutItem({
|
||||||
item,
|
item,
|
||||||
onRemoved,
|
onRemove,
|
||||||
|
images,
|
||||||
}: {
|
}: {
|
||||||
item: Set;
|
item: GymSet
|
||||||
onRemoved: () => void;
|
onRemove: () => void
|
||||||
|
images: boolean
|
||||||
}) {
|
}) {
|
||||||
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 navigation = useNavigation<NavigationProp<WorkoutsPageParams>>()
|
||||||
const navigation = useNavigation<NavigationProp<WorkoutsPageParams>>();
|
|
||||||
|
|
||||||
const remove = useCallback(async () => {
|
const remove = useCallback(async () => {
|
||||||
await deleteSetsBy(item.name);
|
await setRepo.delete({name: item.name})
|
||||||
setShowMenu(false);
|
setShowMenu(false)
|
||||||
onRemoved();
|
onRemove()
|
||||||
}, [setShowMenu, onRemoved, 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)
|
return `${item.sets} x ${item.minutes || 0}:${seconds}`
|
||||||
return `${item.sets} x ${item.minutes || 0}:${seconds}`;
|
}, [item])
|
||||||
else if (settings.alarm && !settings.showSets)
|
|
||||||
return `${item.minutes || 0}:${seconds}`;
|
|
||||||
return `${item.sets}`;
|
|
||||||
}, [item, settings]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -52,7 +48,7 @@ export default function WorkoutItem({
|
||||||
description={description}
|
description={description}
|
||||||
onLongPress={longPress}
|
onLongPress={longPress}
|
||||||
left={() =>
|
left={() =>
|
||||||
!!settings.images &&
|
images &&
|
||||||
item.image && (
|
item.image && (
|
||||||
<Image source={{uri: item.image}} style={{height: 75, width: 75}} />
|
<Image source={{uri: item.image}} style={{height: 75, width: 75}} />
|
||||||
)
|
)
|
||||||
|
@ -69,8 +65,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 +83,5 @@ export default function WorkoutItem({
|
||||||
sure?
|
sure?
|
||||||
</ConfirmDialog>
|
</ConfirmDialog>
|
||||||
</>
|
</>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
137
WorkoutList.tsx
|
@ -2,87 +2,106 @@ import {
|
||||||
NavigationProp,
|
NavigationProp,
|
||||||
useFocusEffect,
|
useFocusEffect,
|
||||||
useNavigation,
|
useNavigation,
|
||||||
} from '@react-navigation/native';
|
} from '@react-navigation/native'
|
||||||
import React, {useCallback, useEffect, 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 Set from './set';
|
import GymSet from './gym-set'
|
||||||
import {getDistinctSets} from './set.service';
|
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, settingsRepo} from './db'
|
||||||
|
import Settings from './settings'
|
||||||
|
|
||||||
const limit = 15;
|
const limit = 15
|
||||||
|
|
||||||
export default function WorkoutList() {
|
export default function WorkoutList() {
|
||||||
const [workouts, setWorkouts] = useState<Set[]>();
|
const [workouts, setWorkouts] = useState<GymSet[]>()
|
||||||
const [offset, setOffset] = useState(0);
|
const [offset, setOffset] = useState(0)
|
||||||
const [search, setSearch] = useState('');
|
const [term, setTerm] = useState('')
|
||||||
const [end, setEnd] = useState(false);
|
const [end, setEnd] = useState(false)
|
||||||
const navigation = useNavigation<NavigationProp<WorkoutsPageParams>>();
|
const [settings, setSettings] = useState<Settings>()
|
||||||
|
const navigation = useNavigation<NavigationProp<WorkoutsPageParams>>()
|
||||||
|
|
||||||
const refresh = useCallback(async () => {
|
const refresh = useCallback(async (value: string) => {
|
||||||
const newWorkouts = await getDistinctSets({
|
const newWorkouts = await setRepo
|
||||||
search: `%${search}%`,
|
.createQueryBuilder()
|
||||||
limit,
|
.select()
|
||||||
offset: 0,
|
.where('name LIKE :name', {name: `%${value}%`})
|
||||||
});
|
.groupBy('name')
|
||||||
console.log(`${WorkoutList.name}`, {newWorkout: newWorkouts[0]});
|
.orderBy('name')
|
||||||
setWorkouts(newWorkouts);
|
.limit(limit)
|
||||||
setOffset(0);
|
.getMany()
|
||||||
setEnd(false);
|
console.log(`${WorkoutList.name}`, {newWorkout: newWorkouts[0]})
|
||||||
}, [search]);
|
setWorkouts(newWorkouts)
|
||||||
|
setOffset(0)
|
||||||
useEffect(() => {
|
setEnd(false)
|
||||||
refresh();
|
}, [])
|
||||||
}, [search, refresh]);
|
|
||||||
|
|
||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
refresh();
|
refresh(term)
|
||||||
}, [refresh]),
|
settingsRepo.findOne({where: {}}).then(setSettings)
|
||||||
);
|
}, [refresh, term]),
|
||||||
|
)
|
||||||
|
|
||||||
const renderItem = useCallback(
|
const renderItem = useCallback(
|
||||||
({item}: {item: Set}) => (
|
({item}: {item: GymSet}) => (
|
||||||
<WorkoutItem item={item} key={item.name} onRemoved={refresh} />
|
<WorkoutItem
|
||||||
|
images={settings?.images}
|
||||||
|
item={item}
|
||||||
|
key={item.name}
|
||||||
|
onRemove={() => refresh(term)}
|
||||||
|
/>
|
||||||
),
|
),
|
||||||
[refresh],
|
[refresh, term, settings?.images],
|
||||||
);
|
)
|
||||||
|
|
||||||
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,
|
||||||
search,
|
term,
|
||||||
});
|
})
|
||||||
const newWorkouts = await getDistinctSets({
|
const newWorkouts = await setRepo
|
||||||
search: `%${search}%`,
|
.createQueryBuilder()
|
||||||
limit,
|
.select()
|
||||||
offset: newOffset,
|
.where('name LIKE :name', {name: `%${term}%`})
|
||||||
});
|
.groupBy('name')
|
||||||
if (newWorkouts.length === 0) return setEnd(true);
|
.orderBy('name')
|
||||||
if (!workouts) return;
|
.limit(limit)
|
||||||
setWorkouts([...workouts, ...newWorkouts]);
|
.offset(newOffset)
|
||||||
if (newWorkouts.length < limit) return setEnd(true);
|
.getMany()
|
||||||
setOffset(newOffset);
|
if (newWorkouts.length === 0) return setEnd(true)
|
||||||
}, [search, end, offset, workouts]);
|
if (!workouts) return
|
||||||
|
setWorkouts([...workouts, ...newWorkouts])
|
||||||
|
if (newWorkouts.length < limit) return setEnd(true)
|
||||||
|
setOffset(newOffset)
|
||||||
|
}, [term, end, offset, workouts])
|
||||||
|
|
||||||
const onAdd = useCallback(async () => {
|
const onAdd = useCallback(async () => {
|
||||||
navigation.navigate('EditWorkout', {
|
navigation.navigate('EditWorkout', {
|
||||||
value: {name: '', sets: 3, image: '', steps: '', reps: 0, weight: 0},
|
value: new GymSet(),
|
||||||
});
|
})
|
||||||
}, [navigation]);
|
}, [navigation])
|
||||||
|
|
||||||
|
const search = useCallback(
|
||||||
|
(value: string) => {
|
||||||
|
setTerm(value)
|
||||||
|
refresh(value)
|
||||||
|
},
|
||||||
|
[refresh],
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DrawerHeader name="Workouts" />
|
<DrawerHeader name="Workouts" />
|
||||||
<Page onAdd={onAdd} search={search} setSearch={setSearch}>
|
<Page onAdd={onAdd} term={term} search={search}>
|
||||||
{workouts?.length === 0 ? (
|
{workouts?.length === 0 ? (
|
||||||
<List.Item
|
<List.Item
|
||||||
title="No workouts yet."
|
title="No workouts yet."
|
||||||
|
@ -99,5 +118,5 @@ export default function WorkoutList() {
|
||||||
)}
|
)}
|
||||||
</Page>
|
</Page>
|
||||||
</>
|
</>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,17 +1,16 @@
|
||||||
import {createStackNavigator} from '@react-navigation/stack';
|
import {createStackNavigator} from '@react-navigation/stack'
|
||||||
import React from 'react';
|
import EditWorkout from './EditWorkout'
|
||||||
import EditWorkout from './EditWorkout';
|
import GymSet from './gym-set'
|
||||||
import Set from './set';
|
import WorkoutList from './WorkoutList'
|
||||||
import WorkoutList from './WorkoutList';
|
|
||||||
|
|
||||||
export type WorkoutsPageParams = {
|
export type WorkoutsPageParams = {
|
||||||
WorkoutList: {};
|
WorkoutList: {}
|
||||||
EditWorkout: {
|
EditWorkout: {
|
||||||
value: Set;
|
value: GymSet
|
||||||
};
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const Stack = createStackNavigator<WorkoutsPageParams>();
|
const Stack = createStackNavigator<WorkoutsPageParams>()
|
||||||
|
|
||||||
export default function WorkoutsPage() {
|
export default function WorkoutsPage() {
|
||||||
return (
|
return (
|
||||||
|
@ -20,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>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ apply plugin: "kotlin-android"
|
||||||
import com.android.build.OutputFile
|
import com.android.build.OutputFile
|
||||||
|
|
||||||
project.ext.react = [
|
project.ext.react = [
|
||||||
enableHermes: false, // clean and rebuild if changing
|
enableHermes: true, // clean and rebuild if changing
|
||||||
]
|
]
|
||||||
|
|
||||||
project.ext.vectoricons = [
|
project.ext.vectoricons = [
|
||||||
|
@ -17,7 +17,7 @@ apply from: "../../node_modules/react-native-vector-icons/fonts.gradle"
|
||||||
def enableSeparateBuildPerCPUArchitecture = true
|
def enableSeparateBuildPerCPUArchitecture = true
|
||||||
def enableProguardInReleaseBuilds = true
|
def enableProguardInReleaseBuilds = true
|
||||||
def jscFlavor = 'org.webkit:android-jsc:+'
|
def jscFlavor = 'org.webkit:android-jsc:+'
|
||||||
def enableHermes = project.ext.react.get("enableHermes", false);
|
def enableHermes = project.ext.react.get("enableHermes", true);
|
||||||
|
|
||||||
def reactNativeArchitectures() {
|
def reactNativeArchitectures() {
|
||||||
def value = project.getProperties().get("reactNativeArchitectures")
|
def value = project.getProperties().get("reactNativeArchitectures")
|
||||||
|
@ -43,8 +43,8 @@ android {
|
||||||
missingDimensionStrategy "RNNotifications.reactNativeVersion", "reactNative60"
|
missingDimensionStrategy "RNNotifications.reactNativeVersion", "reactNative60"
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 36071
|
versionCode 36082
|
||||||
versionName "1.45"
|
versionName "1.56"
|
||||||
buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString()
|
buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString()
|
||||||
|
|
||||||
if (isNewArchitectureEnabled()) {
|
if (isNewArchitectureEnabled()) {
|
||||||
|
|
3
android/app/proguard-rules.pro
vendored
|
@ -44,3 +44,6 @@
|
||||||
-dontwarn java.nio.file.*
|
-dontwarn java.nio.file.*
|
||||||
-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
|
-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
|
||||||
-dontwarn okio.**
|
-dontwarn okio.**
|
||||||
|
|
||||||
|
-keep class com.facebook.hermes.unicode.** { *; }
|
||||||
|
-keep class com.facebook.jni.** { *; }
|
||||||
|
|
|
@ -1,9 +1,15 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
presets: ['module:metro-react-native-babel-preset'],
|
presets: ['module:metro-react-native-babel-preset'],
|
||||||
plugins: ['react-native-reanimated/plugin', 'react-native-paper/babel'],
|
plugins: [
|
||||||
|
'@babel/plugin-transform-flow-strip-types',
|
||||||
|
['@babel/plugin-proposal-decorators', {legacy: true}],
|
||||||
|
['@babel/plugin-proposal-class-properties', {loose: true}],
|
||||||
|
'react-native-reanimated/plugin',
|
||||||
|
'react-native-paper/babel',
|
||||||
|
],
|
||||||
env: {
|
env: {
|
||||||
production: {
|
production: {
|
||||||
plugins: ['transform-remove-console'],
|
plugins: ['transform-remove-console'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
}
|
||||||
|
|
130
best.service.ts
|
@ -1,117 +1,15 @@
|
||||||
import {db} from './db';
|
import {setRepo} from './db'
|
||||||
import {Periods} from './periods';
|
import GymSet from './gym-set'
|
||||||
import Set from './set';
|
|
||||||
import {defaultSet} from './set.service';
|
|
||||||
import Volume from './volume';
|
|
||||||
|
|
||||||
export const getOneRepMax = async ({
|
export const getBestSet = async (name: string): Promise<GymSet> => {
|
||||||
name,
|
return setRepo
|
||||||
period,
|
.createQueryBuilder()
|
||||||
}: {
|
.select()
|
||||||
name: string;
|
.addSelect('MAX(weight)', 'weight')
|
||||||
period: Periods;
|
.where('name = :name', {name})
|
||||||
}) => {
|
.groupBy('name')
|
||||||
// Brzycki formula https://en.wikipedia.org/wiki/One-repetition_maximum#Brzycki
|
.addGroupBy('reps')
|
||||||
const select = `
|
.orderBy('weight', 'DESC')
|
||||||
SELECT max(weight / (1.0278 - 0.0278 * reps)) AS weight,
|
.addOrderBy('reps', 'DESC')
|
||||||
STRFTIME('%Y-%m-%d', created) as created, unit
|
.getOne()
|
||||||
FROM sets
|
}
|
||||||
WHERE name = ? AND NOT hidden
|
|
||||||
AND DATE(created) >= DATE('now', 'weekday 0', ?)
|
|
||||||
GROUP BY name, STRFTIME(?, created)
|
|
||||||
`;
|
|
||||||
let difference = '-7 days';
|
|
||||||
if (period === Periods.Monthly) difference = '-1 months';
|
|
||||||
else if (period === Periods.Yearly) difference = '-1 years';
|
|
||||||
let group = '%Y-%m-%d';
|
|
||||||
if (period === Periods.Yearly) group = '%Y-%m';
|
|
||||||
const [result] = await db.executeSql(select, [name, difference, group]);
|
|
||||||
return result.rows.raw();
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getBestSet = async (name: string): Promise<Set> => {
|
|
||||||
const bestWeight = `
|
|
||||||
SELECT name, reps, unit, MAX(weight) AS weight
|
|
||||||
FROM sets
|
|
||||||
WHERE name = ?
|
|
||||||
GROUP BY name;
|
|
||||||
`;
|
|
||||||
const bestReps = `
|
|
||||||
SELECT name, MAX(reps) as reps, unit, weight, sets, minutes, seconds, image
|
|
||||||
FROM sets
|
|
||||||
WHERE name = ? AND weight = ?
|
|
||||||
GROUP BY name;
|
|
||||||
`;
|
|
||||||
const [weightResult] = await db.executeSql(bestWeight, [name]);
|
|
||||||
if (!weightResult.rows.length) return {...defaultSet};
|
|
||||||
const [repsResult] = await db.executeSql(bestReps, [
|
|
||||||
name,
|
|
||||||
weightResult.rows.item(0).weight,
|
|
||||||
]);
|
|
||||||
return repsResult.rows.item(0);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getWeightsBy = async (
|
|
||||||
name: string,
|
|
||||||
period: Periods,
|
|
||||||
): Promise<Set[]> => {
|
|
||||||
const select = `
|
|
||||||
SELECT max(weight) AS weight,
|
|
||||||
STRFTIME('%Y-%m-%d', created) as created, unit
|
|
||||||
FROM sets
|
|
||||||
WHERE name = ? AND NOT hidden
|
|
||||||
AND DATE(created) >= DATE('now', 'weekday 0', ?)
|
|
||||||
GROUP BY name, STRFTIME(?, created)
|
|
||||||
`;
|
|
||||||
let difference = '-7 days';
|
|
||||||
if (period === Periods.Monthly) difference = '-1 months';
|
|
||||||
else if (period === Periods.Yearly) difference = '-1 years';
|
|
||||||
let group = '%Y-%m-%d';
|
|
||||||
if (period === Periods.Yearly) group = '%Y-%m';
|
|
||||||
const [result] = await db.executeSql(select, [name, difference, group]);
|
|
||||||
return result.rows.raw();
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getVolumes = async (
|
|
||||||
name: string,
|
|
||||||
period: Periods,
|
|
||||||
): Promise<Volume[]> => {
|
|
||||||
const select = `
|
|
||||||
SELECT sum(weight * reps) AS value,
|
|
||||||
STRFTIME('%Y-%m-%d', created) as created, unit
|
|
||||||
FROM sets
|
|
||||||
WHERE name = ? AND NOT hidden
|
|
||||||
AND DATE(created) >= DATE('now', 'weekday 0', ?)
|
|
||||||
GROUP BY name, STRFTIME('%Y-%m-%d', created)
|
|
||||||
`;
|
|
||||||
let difference = '-7 days';
|
|
||||||
if (period === Periods.Monthly) difference = '-1 months';
|
|
||||||
else if (period === Periods.Yearly) difference = '-1 years';
|
|
||||||
const [result] = await db.executeSql(select, [name, difference]);
|
|
||||||
return result.rows.raw();
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getBestWeights = async (search: string): Promise<Set[]> => {
|
|
||||||
const select = `
|
|
||||||
SELECT name, reps, unit, MAX(weight) AS weight
|
|
||||||
FROM sets
|
|
||||||
WHERE name LIKE ? AND NOT hidden
|
|
||||||
GROUP BY name;
|
|
||||||
`;
|
|
||||||
const [result] = await db.executeSql(select, [`%${search}%`]);
|
|
||||||
return result.rows.raw();
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getBestReps = async (
|
|
||||||
name: string,
|
|
||||||
weight: number,
|
|
||||||
): Promise<Set[]> => {
|
|
||||||
const select = `
|
|
||||||
SELECT name, MAX(reps) as reps, unit, weight, image
|
|
||||||
FROM sets
|
|
||||||
WHERE name = ? AND weight = ? AND NOT hidden
|
|
||||||
GROUP BY name;
|
|
||||||
`;
|
|
||||||
const [result] = await db.executeSql(select, [name, weight]);
|
|
||||||
return result.rows.raw();
|
|
||||||
};
|
|
||||||
|
|
11
color.ts
|
@ -1,11 +0,0 @@
|
||||||
import React, {useContext} from 'react';
|
|
||||||
|
|
||||||
export const Color = React.createContext({
|
|
||||||
color: '',
|
|
||||||
setColor: (_value: string) => {},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const useColor = () => {
|
|
||||||
const context = useContext(Color);
|
|
||||||
return context;
|
|
||||||
};
|
|
44
colors.ts
|
@ -1,36 +1,32 @@
|
||||||
export const lightColors = [
|
export const lightColors = [
|
||||||
{hex: '#FA8072', name: 'Salmon'},
|
'#B3E5FC',
|
||||||
{hex: '#B3E5FC', name: 'Cyan'},
|
'#FA8072',
|
||||||
{hex: '#FFC0CB', name: 'Pink'},
|
'#FFC0CB',
|
||||||
{hex: '#E9DCC9', name: 'Linen'},
|
'#E9DCC9',
|
||||||
];
|
'#BBA1CE',
|
||||||
|
]
|
||||||
|
|
||||||
export const darkColors = [
|
export const darkColors = ['#8156A7', '#007AFF', '#000000', '#CD5C5C']
|
||||||
{hex: '#8156A7', name: 'Purple'},
|
|
||||||
{hex: '#007AFF', name: 'Blue'},
|
|
||||||
{hex: '#000000', name: 'Black'},
|
|
||||||
{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}`
|
||||||
};
|
}
|
||||||
|
|
|
@ -1,2 +1,2 @@
|
||||||
export const MARGIN = 10;
|
export const MARGIN = 10
|
||||||
export const PADDING = 10;
|
export const PADDING = 10
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
export default interface CountMany {
|
export default interface CountMany {
|
||||||
name: string;
|
name: string
|
||||||
total: number;
|
total: number
|
||||||
sets?: number;
|
sets?: number
|
||||||
}
|
}
|
||||||
|
|
61
data-source.ts
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
import {DataSource} from 'typeorm'
|
||||||
|
import GymSet from './gym-set'
|
||||||
|
import {Sets1667185586014 as sets1667185586014} from './migrations/1667185586014-sets'
|
||||||
|
import {plans1667186124792} from './migrations/1667186124792-plans'
|
||||||
|
import {settings1667186130041} from './migrations/1667186130041-settings'
|
||||||
|
import {addSound1667186139844} from './migrations/1667186139844-add-sound'
|
||||||
|
import {addHidden1667186159379} from './migrations/1667186159379-add-hidden'
|
||||||
|
import {addNotify1667186166140} from './migrations/1667186166140-add-notify'
|
||||||
|
import {addImage1667186171548} from './migrations/1667186171548-add-image'
|
||||||
|
import {addImages1667186179488} from './migrations/1667186179488-add-images'
|
||||||
|
import {insertSettings1667186203827} from './migrations/1667186203827-insert-settings'
|
||||||
|
import {addSteps1667186211251} from './migrations/1667186211251-add-steps'
|
||||||
|
import {addSets1667186250618} from './migrations/1667186250618-add-sets'
|
||||||
|
import {addMinutes1667186255650} from './migrations/1667186255650-add-minutes'
|
||||||
|
import {addSeconds1667186259174} from './migrations/1667186259174-add-seconds'
|
||||||
|
import {addShowUnit1667186265588} from './migrations/1667186265588-add-show-unit'
|
||||||
|
import {addColor1667186320954} from './migrations/1667186320954-add-color'
|
||||||
|
import {addSteps1667186348425} from './migrations/1667186348425-add-steps'
|
||||||
|
import {addDate1667186431804} from './migrations/1667186431804-add-date'
|
||||||
|
import {addShowDate1667186435051} from './migrations/1667186435051-add-show-date'
|
||||||
|
import {addTheme1667186439366} from './migrations/1667186439366-add-theme'
|
||||||
|
import {addShowSets1667186443614} from './migrations/1667186443614-add-show-sets'
|
||||||
|
import {addSetsCreated1667186451005} from './migrations/1667186451005-add-sets-created'
|
||||||
|
import {addNoSound1667186456118} from './migrations/1667186456118-add-no-sound'
|
||||||
|
import {dropMigrations1667190214743} from './migrations/1667190214743-drop-migrations'
|
||||||
|
import {Plan} from './plan'
|
||||||
|
import Settings from './settings'
|
||||||
|
|
||||||
|
export const AppDataSource = new DataSource({
|
||||||
|
type: 'react-native',
|
||||||
|
database: 'massive.db',
|
||||||
|
location: 'default',
|
||||||
|
entities: [GymSet, Plan, Settings],
|
||||||
|
migrationsRun: true,
|
||||||
|
migrationsTableName: 'typeorm_migrations',
|
||||||
|
migrations: [
|
||||||
|
sets1667185586014,
|
||||||
|
plans1667186124792,
|
||||||
|
settings1667186130041,
|
||||||
|
addSound1667186139844,
|
||||||
|
addHidden1667186159379,
|
||||||
|
addNotify1667186166140,
|
||||||
|
addImage1667186171548,
|
||||||
|
addImages1667186179488,
|
||||||
|
insertSettings1667186203827,
|
||||||
|
addSteps1667186211251,
|
||||||
|
addSets1667186250618,
|
||||||
|
addMinutes1667186255650,
|
||||||
|
addSeconds1667186259174,
|
||||||
|
addShowUnit1667186265588,
|
||||||
|
addColor1667186320954,
|
||||||
|
addSteps1667186348425,
|
||||||
|
addDate1667186431804,
|
||||||
|
addShowDate1667186435051,
|
||||||
|
addTheme1667186439366,
|
||||||
|
addShowSets1667186443614,
|
||||||
|
addSetsCreated1667186451005,
|
||||||
|
addNoSound1667186456118,
|
||||||
|
dropMigrations1667190214743,
|
||||||
|
],
|
||||||
|
})
|
157
db.ts
|
@ -1,151 +1,14 @@
|
||||||
import {
|
import {AppDataSource} from './data-source'
|
||||||
enablePromise,
|
import GymSet from './gym-set'
|
||||||
openDatabase,
|
import {Plan} from './plan'
|
||||||
SQLiteDatabase,
|
import Settings from './settings'
|
||||||
} from 'react-native-sqlite-storage';
|
|
||||||
|
|
||||||
enablePromise(true);
|
export const setRepo = AppDataSource.manager.getRepository(GymSet)
|
||||||
|
export const planRepo = AppDataSource.manager.getRepository(Plan)
|
||||||
|
export const settingsRepo = AppDataSource.manager.getRepository(Settings)
|
||||||
|
|
||||||
const migrations = [
|
export const getNow = (): Promise<{now: string}[]> => {
|
||||||
`
|
return AppDataSource.manager.query(
|
||||||
CREATE TABLE IF NOT EXISTS sets (
|
"SELECT STRFTIME('%Y-%m-%dT%H:%M:%S','now','localtime') AS now",
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
name TEXT NOT NULL,
|
|
||||||
reps INTEGER NOT NULL,
|
|
||||||
weight INTEGER NOT NULL,
|
|
||||||
created TEXT NOT NULL,
|
|
||||||
unit TEXT DEFAULT 'kg'
|
|
||||||
)
|
)
|
||||||
`,
|
|
||||||
`
|
|
||||||
CREATE TABLE IF NOT EXISTS plans (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
days TEXT NOT NULL,
|
|
||||||
workouts TEXT NOT NULL
|
|
||||||
)
|
|
||||||
`,
|
|
||||||
`
|
|
||||||
CREATE TABLE IF NOT EXISTS settings (
|
|
||||||
minutes INTEGER NOT NULL DEFAULT 3,
|
|
||||||
seconds INTEGER NOT NULL DEFAULT 30,
|
|
||||||
alarm BOOLEAN NOT NULL DEFAULT 0,
|
|
||||||
vibrate BOOLEAN NOT NULL DEFAULT 1,
|
|
||||||
sets INTEGER NOT NULL DEFAULT 3
|
|
||||||
)
|
|
||||||
`,
|
|
||||||
`ALTER TABLE settings ADD COLUMN sound TEXT NULL`,
|
|
||||||
`
|
|
||||||
CREATE TABLE IF NOT EXISTS workouts(
|
|
||||||
name TEXT PRIMARY KEY,
|
|
||||||
sets INTEGER DEFAULT 3
|
|
||||||
)
|
|
||||||
`,
|
|
||||||
`
|
|
||||||
ALTER TABLE sets ADD COLUMN hidden DEFAULT false
|
|
||||||
`,
|
|
||||||
`
|
|
||||||
ALTER TABLE settings ADD COLUMN notify DEFAULT false
|
|
||||||
`,
|
|
||||||
`
|
|
||||||
ALTER TABLE sets ADD COLUMN image TEXT NULL
|
|
||||||
`,
|
|
||||||
`
|
|
||||||
ALTER TABLE settings ADD COLUMN images BOOLEAN DEFAULT true
|
|
||||||
`,
|
|
||||||
`
|
|
||||||
SELECT * FROM settings LIMIT 1
|
|
||||||
`,
|
|
||||||
`
|
|
||||||
INSERT INTO settings(minutes) VALUES(3)
|
|
||||||
`,
|
|
||||||
`
|
|
||||||
ALTER TABLE workouts ADD COLUMN steps TEXT NULL
|
|
||||||
`,
|
|
||||||
`
|
|
||||||
INSERT OR IGNORE INTO workouts (name) SELECT DISTINCT name FROM sets
|
|
||||||
`,
|
|
||||||
`
|
|
||||||
ALTER TABLE sets ADD COLUMN sets INTEGER NOT NULL DEFAULT 3
|
|
||||||
`,
|
|
||||||
`
|
|
||||||
ALTER TABLE sets ADD COLUMN minutes INTEGER NOT NULL DEFAULT 3
|
|
||||||
`,
|
|
||||||
`
|
|
||||||
ALTER TABLE sets ADD COLUMN seconds INTEGER NOT NULL DEFAULT 30
|
|
||||||
`,
|
|
||||||
`
|
|
||||||
ALTER TABLE settings ADD COLUMN showUnit BOOLEAN DEFAULT true
|
|
||||||
`,
|
|
||||||
`
|
|
||||||
ALTER TABLE sets ADD COLUMN steps TEXT NULL
|
|
||||||
`,
|
|
||||||
`
|
|
||||||
UPDATE sets SET steps = (
|
|
||||||
SELECT workouts.steps FROM workouts WHERE workouts.name = sets.name
|
|
||||||
)
|
|
||||||
`,
|
|
||||||
`
|
|
||||||
DROP TABLE workouts
|
|
||||||
`,
|
|
||||||
`
|
|
||||||
ALTER TABLE settings ADD COLUMN color TEXT NULL
|
|
||||||
`,
|
|
||||||
`
|
|
||||||
UPDATE settings SET showUnit = 1
|
|
||||||
`,
|
|
||||||
`
|
|
||||||
ALTER TABLE settings ADD COLUMN workouts BOOLEAN DEFAULT true
|
|
||||||
`,
|
|
||||||
`
|
|
||||||
ALTER TABLE settings ADD COLUMN steps BOOLEAN DEFAULT true
|
|
||||||
`,
|
|
||||||
`
|
|
||||||
ALTER TABLE settings ADD COLUMN nextAlarm TEXT NULL
|
|
||||||
`,
|
|
||||||
`
|
|
||||||
ALTER TABLE settings ADD COLUMN newSet TEXT NULL
|
|
||||||
`,
|
|
||||||
`
|
|
||||||
ALTER TABLE settings ADD COLUMN date TEXT NULL
|
|
||||||
`,
|
|
||||||
`
|
|
||||||
ALTER TABLE settings ADD COLUMN showDate BOOLEAN DEFAULT false
|
|
||||||
`,
|
|
||||||
`
|
|
||||||
ALTER TABLE settings ADD COLUMN theme TEXT
|
|
||||||
`,
|
|
||||||
`
|
|
||||||
ALTER TABLE settings ADD COLUMN showSets BOOLEAN DEFAULT true
|
|
||||||
`,
|
|
||||||
`
|
|
||||||
CREATE INDEX sets_created ON sets(created)
|
|
||||||
`,
|
|
||||||
`
|
|
||||||
ALTER TABLE settings ADD COLUMN noSound BOOLEAN DEFAULT false
|
|
||||||
`,
|
|
||||||
`
|
|
||||||
CREATE INDEX sets_created ON sets(created)
|
|
||||||
`,
|
|
||||||
];
|
|
||||||
|
|
||||||
export let db: SQLiteDatabase;
|
|
||||||
|
|
||||||
export const runMigrations = async () => {
|
|
||||||
db = await openDatabase({name: 'massive.db'});
|
|
||||||
await db.executeSql(`
|
|
||||||
CREATE TABLE IF NOT EXISTS migrations(
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
command TEXT NOT NULL
|
|
||||||
)
|
|
||||||
`);
|
|
||||||
const [result] = await db.executeSql(`SELECT * FROM migrations`);
|
|
||||||
const missing = migrations.slice(result.rows.length);
|
|
||||||
for (const command of missing) {
|
|
||||||
await db.executeSql(command).catch(console.error);
|
|
||||||
const insert = `
|
|
||||||
INSERT INTO migrations (command)
|
|
||||||
VALUES (?)
|
|
||||||
`;
|
|
||||||
await db.executeSql(insert, [command]);
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
set -ex
|
set -ex
|
||||||
git push origin HEAD > /dev/null &
|
|
||||||
|
yarn tsc
|
||||||
|
yarn lint
|
||||||
|
git push origin HEAD
|
||||||
|
|
||||||
cd android || exit 1
|
cd android || exit 1
|
||||||
build=app/build.gradle
|
build=app/build.gradle
|
||||||
versionCode=$(
|
versionCode=$(
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
export type DrawerParamList = {
|
export type DrawerParamList = {
|
||||||
Home: {};
|
Home: {}
|
||||||
Settings: {};
|
Settings: {}
|
||||||
Best: {};
|
Best: {}
|
||||||
Plans: {};
|
Plans: {}
|
||||||
Workouts: {};
|
Workouts: {}
|
||||||
Timer: {};
|
}
|
||||||
};
|
|
||||||
|
|
40
gym-set.ts
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
import {Column, Entity, PrimaryGeneratedColumn} from 'typeorm'
|
||||||
|
|
||||||
|
@Entity('sets')
|
||||||
|
export default class GymSet {
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
id?: number
|
||||||
|
|
||||||
|
@Column('text')
|
||||||
|
name: string
|
||||||
|
|
||||||
|
@Column('int')
|
||||||
|
reps: number
|
||||||
|
|
||||||
|
@Column('int')
|
||||||
|
weight: number
|
||||||
|
|
||||||
|
@Column('int')
|
||||||
|
sets = 3
|
||||||
|
|
||||||
|
@Column('int')
|
||||||
|
minutes = 3
|
||||||
|
|
||||||
|
@Column('int')
|
||||||
|
seconds = 30
|
||||||
|
|
||||||
|
@Column('boolean')
|
||||||
|
hidden = false
|
||||||
|
|
||||||
|
@Column('text')
|
||||||
|
created: string
|
||||||
|
|
||||||
|
@Column('text')
|
||||||
|
unit: string
|
||||||
|
|
||||||
|
@Column('text')
|
||||||
|
image: string
|
||||||
|
|
||||||
|
@Column('text')
|
||||||
|
steps?: string
|
||||||
|
}
|
|
@ -1,8 +1,8 @@
|
||||||
import Set from './set';
|
import GymSet from './gym-set'
|
||||||
|
|
||||||
export type HomePageParams = {
|
export type HomePageParams = {
|
||||||
Sets: {};
|
Sets: {}
|
||||||
EditSet: {
|
EditSet: {
|
||||||
set: Set;
|
set: GymSet
|
||||||
};
|
}
|
||||||
};
|
}
|
||||||
|
|
11
index.js
|
@ -1,7 +1,6 @@
|
||||||
import {AppRegistry} from 'react-native';
|
import {AppRegistry} from 'react-native'
|
||||||
import 'react-native-gesture-handler';
|
import 'react-native-gesture-handler'
|
||||||
import 'react-native-sqlite-storage';
|
import App from './App'
|
||||||
import App from './App';
|
import {name as appName} from './app.json'
|
||||||
import {name as appName} from './app.json';
|
|
||||||
|
|
||||||
AppRegistry.registerComponent(appName, () => App);
|
AppRegistry.registerComponent(appName, () => App)
|
||||||
|
|
6
input.ts
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
cd android || exit 1
|
set -ex
|
||||||
./gradlew assembleRelease
|
cd android
|
||||||
|
[ "$1" != "--nobuild" ] && ./gradlew assembleRelease
|
||||||
adb -d install app/build/outputs/apk/release/app-arm64-v8a-release.apk
|
adb -d install app/build/outputs/apk/release/app-arm64-v8a-release.apk
|
||||||
|
|
14
jest.config.js
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
module.exports = {
|
||||||
|
preset: 'react-native',
|
||||||
|
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
|
||||||
|
transform: {
|
||||||
|
'^.+\\.(js|jsx)$': 'babel-jest',
|
||||||
|
},
|
||||||
|
transformIgnorePatterns: [
|
||||||
|
'node_modules/(?!(jest-)?@?react-native|@react-native-community|@react-navigation)',
|
||||||
|
],
|
||||||
|
setupFiles: [
|
||||||
|
'./node_modules/react-native-gesture-handler/jestSetup',
|
||||||
|
'./jestSetup.ts',
|
||||||
|
],
|
||||||
|
}
|
29
jestSetup.ts
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import 'react-native-gesture-handler/jestSetup'
|
||||||
|
import {NativeModules as RNNativeModules} from 'react-native'
|
||||||
|
|
||||||
|
//RNNativeModules.UIManager = RNNativeModules.UIManager || {};
|
||||||
|
//RNNativeModules.UIManager.RCTView = RNNativeModules.UIManager.RCTView || {};
|
||||||
|
//RNNativeModules.RNGestureHandlerModule =
|
||||||
|
// RNNativeModules.RNGestureHandlerModule || {
|
||||||
|
// State: {BEGAN: 'BEGAN', FAILED: 'FAILED', ACTIVE: 'ACTIVE', END: 'END'},
|
||||||
|
// attachGestureHandler: jest.fn(),
|
||||||
|
// createGestureHandler: jest.fn(),
|
||||||
|
// dropGestureHandler: jest.fn(),
|
||||||
|
// updateGestureHandler: jest.fn(),
|
||||||
|
// };
|
||||||
|
//RNNativeModules.PlatformConstants = RNNativeModules.PlatformConstants || {
|
||||||
|
// forceTouchAvailable: false,
|
||||||
|
//};
|
||||||
|
RNNativeModules.RNViewShot = RNNativeModules.RNViewShot || {
|
||||||
|
captureScreen: jest.fn(),
|
||||||
|
}
|
||||||
|
|
||||||
|
jest.mock('react-native-file-access', () => jest.fn())
|
||||||
|
jest.mock('react-native-share', () => jest.fn())
|
||||||
|
jest.mock('react-native/Libraries/Animated/NativeAnimatedHelper')
|
||||||
|
jest.useFakeTimers()
|
||||||
|
jest.mock('react-native-reanimated', () => {
|
||||||
|
const Reanimated = require('react-native-reanimated/mock')
|
||||||
|
Reanimated.default.call = () => {}
|
||||||
|
return Reanimated
|
||||||
|
})
|
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 103 KiB |
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 58 KiB |
Before Width: | Height: | Size: 117 KiB After Width: | Height: | Size: 132 KiB |
Before Width: | Height: | Size: 118 KiB After Width: | Height: | Size: 138 KiB |
Before Width: | Height: | Size: 178 KiB After Width: | Height: | Size: 198 KiB |
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 106 KiB |
Before Width: | Height: | Size: 93 KiB After Width: | Height: | Size: 132 KiB |
Before Width: | Height: | Size: 126 KiB After Width: | Height: | Size: 163 KiB |
|
@ -14,4 +14,4 @@ module.exports = {
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
};
|
}
|
||||||
|
|
20
migrations/1667185586014-sets.ts
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import {MigrationInterface, QueryRunner} from 'typeorm'
|
||||||
|
|
||||||
|
export class Sets1667185586014 implements MigrationInterface {
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS sets (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
reps INTEGER NOT NULL,
|
||||||
|
weight INTEGER NOT NULL,
|
||||||
|
created TEXT NOT NULL,
|
||||||
|
unit TEXT DEFAULT 'kg'
|
||||||
|
)
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query('DROP TABLE sets')
|
||||||
|
}
|
||||||
|
}
|
17
migrations/1667186124792-plans.ts
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import {MigrationInterface, QueryRunner} from 'typeorm'
|
||||||
|
|
||||||
|
export class plans1667186124792 implements MigrationInterface {
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS plans (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
days TEXT NOT NULL,
|
||||||
|
workouts TEXT NOT NULL
|
||||||
|
)
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.dropTable('plans')
|
||||||
|
}
|
||||||
|
}
|
19
migrations/1667186130041-settings.ts
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import {MigrationInterface, QueryRunner} from 'typeorm'
|
||||||
|
|
||||||
|
export class settings1667186130041 implements MigrationInterface {
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS settings (
|
||||||
|
minutes INTEGER NOT NULL DEFAULT 3,
|
||||||
|
seconds INTEGER NOT NULL DEFAULT 30,
|
||||||
|
alarm BOOLEAN NOT NULL DEFAULT 0,
|
||||||
|
vibrate BOOLEAN NOT NULL DEFAULT 1,
|
||||||
|
sets INTEGER NOT NULL DEFAULT 3
|
||||||
|
)
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.dropTable('settings')
|
||||||
|
}
|
||||||
|
}
|
13
migrations/1667186139844-add-sound.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import {MigrationInterface, QueryRunner} from 'typeorm'
|
||||||
|
|
||||||
|
export class addSound1667186139844 implements MigrationInterface {
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner
|
||||||
|
.query('ALTER TABLE settings ADD COLUMN sound TEXT NULL')
|
||||||
|
.catch(() => null)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.dropColumn('settings', 'sound')
|
||||||
|
}
|
||||||
|
}
|
13
migrations/1667186159379-add-hidden.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import {MigrationInterface, QueryRunner} from 'typeorm'
|
||||||
|
|
||||||
|
export class addHidden1667186159379 implements MigrationInterface {
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner
|
||||||
|
.query('ALTER TABLE sets ADD COLUMN hidden DEFAULT false')
|
||||||
|
.catch(() => null)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.dropColumn('sets', 'hidden')
|
||||||
|
}
|
||||||
|
}
|
13
migrations/1667186166140-add-notify.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import {MigrationInterface, QueryRunner} from 'typeorm'
|
||||||
|
|
||||||
|
export class addNotify1667186166140 implements MigrationInterface {
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner
|
||||||
|
.query('ALTER TABLE settings ADD COLUMN notify DEFAULT false')
|
||||||
|
.catch(() => null)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.dropColumn('settings', 'notify')
|
||||||
|
}
|
||||||
|
}
|
13
migrations/1667186171548-add-image.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import {MigrationInterface, QueryRunner} from 'typeorm'
|
||||||
|
|
||||||
|
export class addImage1667186171548 implements MigrationInterface {
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner
|
||||||
|
.query('ALTER TABLE sets ADD COLUMN image TEXT NULL')
|
||||||
|
.catch(() => null)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.dropColumn('sets', 'image')
|
||||||
|
}
|
||||||
|
}
|
13
migrations/1667186179488-add-images.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import {MigrationInterface, QueryRunner} from 'typeorm'
|
||||||
|
|
||||||
|
export class addImages1667186179488 implements MigrationInterface {
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner
|
||||||
|
.query('ALTER TABLE settings ADD COLUMN images BOOLEAN DEFAULT true')
|
||||||
|
.catch(() => null)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.dropColumn('settings', 'images')
|
||||||
|
}
|
||||||
|
}
|
11
migrations/1667186203827-insert-settings.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import {MigrationInterface, QueryRunner} from 'typeorm'
|
||||||
|
|
||||||
|
export class insertSettings1667186203827 implements MigrationInterface {
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query('INSERT INTO settings(minutes) VALUES(3)')
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query('DELETE FROM settings')
|
||||||
|
}
|
||||||
|
}
|
13
migrations/1667186211251-add-steps.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import {MigrationInterface, QueryRunner} from 'typeorm'
|
||||||
|
|
||||||
|
export class addSteps1667186211251 implements MigrationInterface {
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner
|
||||||
|
.query('ALTER TABLE settings ADD COLUMN steps BOOLEAN DEFAULT true')
|
||||||
|
.catch(() => null)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.dropColumn('settings', 'steps')
|
||||||
|
}
|
||||||
|
}
|
13
migrations/1667186250618-add-sets.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import {MigrationInterface, QueryRunner} from 'typeorm'
|
||||||
|
|
||||||
|
export class addSets1667186250618 implements MigrationInterface {
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner
|
||||||
|
.query('ALTER TABLE sets ADD COLUMN sets INTEGER NOT NULL DEFAULT 3')
|
||||||
|
.catch(() => null)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.dropColumn('sets', 'sets')
|
||||||
|
}
|
||||||
|
}
|
13
migrations/1667186255650-add-minutes.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import {MigrationInterface, QueryRunner} from 'typeorm'
|
||||||
|
|
||||||
|
export class addMinutes1667186255650 implements MigrationInterface {
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner
|
||||||
|
.query('ALTER TABLE sets ADD COLUMN minutes INTEGER NOT NULL DEFAULT 3')
|
||||||
|
.catch(() => null)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.dropColumn('sets', 'minutes')
|
||||||
|
}
|
||||||
|
}
|
13
migrations/1667186259174-add-seconds.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import {MigrationInterface, QueryRunner} from 'typeorm'
|
||||||
|
|
||||||
|
export class addSeconds1667186259174 implements MigrationInterface {
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner
|
||||||
|
.query('ALTER TABLE sets ADD COLUMN seconds INTEGER NOT NULL DEFAULT 30')
|
||||||
|
.catch(() => null)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.dropColumn('sets', 'seconds')
|
||||||
|
}
|
||||||
|
}
|
13
migrations/1667186265588-add-show-unit.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import {MigrationInterface, QueryRunner} from 'typeorm'
|
||||||
|
|
||||||
|
export class addShowUnit1667186265588 implements MigrationInterface {
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner
|
||||||
|
.query('ALTER TABLE settings ADD COLUMN showUnit BOOLEAN DEFAULT true')
|
||||||
|
.catch(() => null)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.dropColumn('settings', 'showUnit')
|
||||||
|
}
|
||||||
|
}
|
22
migrations/1667186320954-add-color.ts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import {MigrationInterface, QueryRunner, TableColumn} from 'typeorm'
|
||||||
|
import {darkColors} from '../colors'
|
||||||
|
|
||||||
|
export class addColor1667186320954 implements MigrationInterface {
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner
|
||||||
|
.addColumn(
|
||||||
|
'settings',
|
||||||
|
new TableColumn({
|
||||||
|
name: 'color',
|
||||||
|
type: 'text',
|
||||||
|
isNullable: false,
|
||||||
|
default: `'${darkColors[0]}'`,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.catch(console.error)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.dropColumn('settings', 'color')
|
||||||
|
}
|
||||||
|
}
|
13
migrations/1667186348425-add-steps.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import {MigrationInterface, QueryRunner} from 'typeorm'
|
||||||
|
|
||||||
|
export class addSteps1667186348425 implements MigrationInterface {
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner
|
||||||
|
.query('ALTER TABLE sets ADD COLUMN steps TEXT NULL')
|
||||||
|
.catch(() => null)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.dropColumn('sets', 'steps')
|
||||||
|
}
|
||||||
|
}
|
13
migrations/1667186431804-add-date.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import {MigrationInterface, QueryRunner} from 'typeorm'
|
||||||
|
|
||||||
|
export class addDate1667186431804 implements MigrationInterface {
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner
|
||||||
|
.query('ALTER TABLE settings ADD COLUMN date TEXT NULL')
|
||||||
|
.catch(() => null)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.dropColumn('settings', 'date')
|
||||||
|
}
|
||||||
|
}
|
13
migrations/1667186435051-add-show-date.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import {MigrationInterface, QueryRunner} from 'typeorm'
|
||||||
|
|
||||||
|
export class addShowDate1667186435051 implements MigrationInterface {
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner
|
||||||
|
.query('ALTER TABLE settings ADD COLUMN showDate BOOLEAN DEFAULT false')
|
||||||
|
.catch(() => null)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.dropColumn('settings', 'showDate')
|
||||||
|
}
|
||||||
|
}
|
13
migrations/1667186439366-add-theme.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import {MigrationInterface, QueryRunner} from 'typeorm'
|
||||||
|
|
||||||
|
export class addTheme1667186439366 implements MigrationInterface {
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner
|
||||||
|
.query('ALTER TABLE settings ADD COLUMN theme TEXT')
|
||||||
|
.catch(() => null)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.dropColumn('settings', 'theme')
|
||||||
|
}
|
||||||
|
}
|
13
migrations/1667186443614-add-show-sets.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import {MigrationInterface, QueryRunner} from 'typeorm'
|
||||||
|
|
||||||
|
export class addShowSets1667186443614 implements MigrationInterface {
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner
|
||||||
|
.query('ALTER TABLE settings ADD COLUMN showSets BOOLEAN DEFAULT true')
|
||||||
|
.catch(() => null)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.dropColumn('settings', 'showSets')
|
||||||
|
}
|
||||||
|
}
|
13
migrations/1667186451005-add-sets-created.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import {MigrationInterface, QueryRunner} from 'typeorm'
|
||||||
|
|
||||||
|
export class addSetsCreated1667186451005 implements MigrationInterface {
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner
|
||||||
|
.query('CREATE INDEX sets_created ON sets(created)')
|
||||||
|
.catch(() => null)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.dropIndex('sets', 'sets_created')
|
||||||
|
}
|
||||||
|
}
|
13
migrations/1667186456118-add-no-sound.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import {MigrationInterface, QueryRunner} from 'typeorm'
|
||||||
|
|
||||||
|
export class addNoSound1667186456118 implements MigrationInterface {
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner
|
||||||
|
.query('ALTER TABLE settings ADD COLUMN noSound BOOLEAN DEFAULT false')
|
||||||
|
.catch(() => null)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.dropColumn('settings', 'noSound')
|
||||||
|
}
|
||||||
|
}
|
19
migrations/1667190214743-drop-migrations.ts
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import {MigrationInterface, QueryRunner, Table, TableColumn} from 'typeorm'
|
||||||
|
|
||||||
|
export class dropMigrations1667190214743 implements MigrationInterface {
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.dropTable('migrations').catch(() => null)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.createTable(
|
||||||
|
new Table({
|
||||||
|
name: 'migrations',
|
||||||
|
columns: [
|
||||||
|
new TableColumn({name: 'id', type: 'integer'}),
|
||||||
|
new TableColumn({name: 'command', type: 'text'}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
24
mock-providers.tsx
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import {NavigationContainer} from '@react-navigation/native'
|
||||||
|
import React from 'react'
|
||||||
|
import {DefaultTheme, Provider as PaperProvider} from 'react-native-paper'
|
||||||
|
import MaterialIcon from 'react-native-vector-icons/MaterialIcons'
|
||||||
|
import {ThemeContext} from './use-theme'
|
||||||
|
|
||||||
|
export const theme = {
|
||||||
|
theme: 'system',
|
||||||
|
setTheme: jest.fn(),
|
||||||
|
color: DefaultTheme.colors.primary,
|
||||||
|
setColor: jest.fn(),
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MockProviders = ({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: JSX.Element | JSX.Element[]
|
||||||
|
}) => (
|
||||||
|
<PaperProvider settings={{icon: props => <MaterialIcon {...props} />}}>
|
||||||
|
<ThemeContext.Provider value={theme}>
|
||||||
|
<NavigationContainer>{children}</NavigationContainer>
|
||||||
|
</ThemeContext.Provider>
|
||||||
|
</PaperProvider>
|
||||||
|
)
|
17
package.json
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "massive",
|
"name": "massive",
|
||||||
"version": "1.45",
|
"version": "1.56",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "GPL-3.0-only",
|
"license": "GPL-3.0-only",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -8,20 +8,27 @@
|
||||||
"release": "react-native run-android --variant=release",
|
"release": "react-native run-android --variant=release",
|
||||||
"start": "react-native start",
|
"start": "react-native start",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"lint": "eslint . --ext .js,.jsx,.ts,.tsx --quiet"
|
"lint": "eslint . --ext .js,.jsx,.ts,.tsx"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@babel/plugin-transform-flow-strip-types": "^7.19.0",
|
||||||
"@babel/preset-env": "^7.19.1",
|
"@babel/preset-env": "^7.19.1",
|
||||||
"@react-native-masked-view/masked-view": "^0.2.7",
|
"@react-native-masked-view/masked-view": "^0.2.7",
|
||||||
"@react-native-picker/picker": "^2.4.4",
|
"@react-native-picker/picker": "^2.4.4",
|
||||||
"@react-navigation/drawer": "^6.5.0",
|
"@react-navigation/drawer": "^6.5.0",
|
||||||
"@react-navigation/native": "^6.0.13",
|
"@react-navigation/native": "^6.0.13",
|
||||||
"@react-navigation/stack": "^6.3.0",
|
"@react-navigation/stack": "^6.3.0",
|
||||||
|
"@testing-library/jest-native": "^5.1.2",
|
||||||
|
"@testing-library/react-native": "^11.3.0",
|
||||||
"@types/d3-shape": "^3.1.0",
|
"@types/d3-shape": "^3.1.0",
|
||||||
|
"@types/jest": "^29.2.0",
|
||||||
"@types/react-native-sqlite-storage": "^5.0.2",
|
"@types/react-native-sqlite-storage": "^5.0.2",
|
||||||
"@types/react-native-svg-charts": "^5.0.12",
|
"@types/react-native-svg-charts": "^5.0.12",
|
||||||
"@types/react-native-vector-icons": "^6.4.12",
|
"@types/react-native-vector-icons": "^6.4.12",
|
||||||
|
"babel-jest": "^29.2.2",
|
||||||
"babel-plugin-transform-remove-console": "^6.9.4",
|
"babel-plugin-transform-remove-console": "^6.9.4",
|
||||||
|
"eslint-plugin-flowtype": "^8.0.3",
|
||||||
|
"jest": "^29.2.2",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-native": "^0.70.4",
|
"react-native": "^0.70.4",
|
||||||
"react-native-document-picker": "^8.1.2",
|
"react-native-document-picker": "^8.1.2",
|
||||||
|
@ -38,12 +45,16 @@
|
||||||
"react-native-svg": "^13.4.0",
|
"react-native-svg": "^13.4.0",
|
||||||
"react-native-svg-charts": "^5.4.0",
|
"react-native-svg-charts": "^5.4.0",
|
||||||
"react-native-vector-icons": "^9.2.0",
|
"react-native-vector-icons": "^9.2.0",
|
||||||
"react-native-view-shot": "^3.4.0"
|
"react-native-view-shot": "^3.4.0",
|
||||||
|
"react-test-renderer": "^18.2.0",
|
||||||
|
"typeorm": "^0.3.10"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.12.9",
|
"@babel/core": "^7.12.9",
|
||||||
|
"@babel/plugin-proposal-decorators": "^7.20.0",
|
||||||
"@babel/runtime": "^7.12.5",
|
"@babel/runtime": "^7.12.5",
|
||||||
"@react-native-community/eslint-config": "^2.0.0",
|
"@react-native-community/eslint-config": "^2.0.0",
|
||||||
|
"@types/node": "^18.11.7",
|
||||||
"@types/react-native": "^0.69.0",
|
"@types/react-native": "^0.69.0",
|
||||||
"@types/react-test-renderer": "^18.0.0",
|
"@types/react-test-renderer": "^18.0.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.29.0",
|
"@typescript-eslint/eslint-plugin": "^5.29.0",
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
import {Plan} from './plan';
|
import GymSet from './gym-set'
|
||||||
import Set from './set';
|
import {Plan} from './plan'
|
||||||
|
|
||||||
export type PlanPageParams = {
|
export type PlanPageParams = {
|
||||||
PlanList: {};
|
PlanList: {}
|
||||||
EditPlan: {
|
EditPlan: {
|
||||||
plan: Plan;
|
plan: Plan
|
||||||
};
|
}
|
||||||
StartPlan: {
|
StartPlan: {
|
||||||
plan: Plan;
|
plan: Plan
|
||||||
set: Set;
|
}
|
||||||
};
|
EditSet: {
|
||||||
};
|
set: GymSet
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,60 +0,0 @@
|
||||||
import {db} from './db';
|
|
||||||
import {Plan} from './plan';
|
|
||||||
import {DAYS} from './time';
|
|
||||||
|
|
||||||
export const getPlans = async (search: string): Promise<Plan[]> => {
|
|
||||||
const select = `
|
|
||||||
SELECT * from plans
|
|
||||||
WHERE days LIKE ? OR workouts LIKE ?
|
|
||||||
`;
|
|
||||||
const [result] = await db.executeSql(select, [`%${search}%`, `%${search}%`]);
|
|
||||||
return result.rows.raw();
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getTodaysPlan = async (): Promise<Plan[]> => {
|
|
||||||
const today = DAYS[new Date().getDay()];
|
|
||||||
const [result] = await db.executeSql(
|
|
||||||
`SELECT * FROM plans WHERE days LIKE ? LIMIT 1`,
|
|
||||||
[`%${today}%`],
|
|
||||||
);
|
|
||||||
return result.rows.raw();
|
|
||||||
};
|
|
||||||
|
|
||||||
export const updatePlanWorkouts = async (oldName: string, newName: string) => {
|
|
||||||
const update = `
|
|
||||||
UPDATE plans SET workouts = REPLACE(workouts, ?, ?)
|
|
||||||
WHERE workouts LIKE ?
|
|
||||||
`;
|
|
||||||
return db.executeSql(update, [oldName, newName, `%${oldName}%`]);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const updatePlan = async (value: Plan) => {
|
|
||||||
const update = `UPDATE plans SET days = ?, workouts = ? WHERE id = ?`;
|
|
||||||
return db.executeSql(update, [value.days, value.workouts, value.id]);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const addPlan = async (value: Plan) => {
|
|
||||||
const insert = `INSERT INTO plans(days, workouts) VALUES (?, ?)`;
|
|
||||||
return db.executeSql(insert, [value.days, value.workouts]);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const addPlans = async (values: string) => {
|
|
||||||
const insert = `
|
|
||||||
INSERT INTO plans(days,workouts) VALUES ${values}
|
|
||||||
`;
|
|
||||||
return db.executeSql(insert);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const deletePlans = async () => {
|
|
||||||
return db.executeSql(`DELETE FROM plans`);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const deletePlan = async (id: number) => {
|
|
||||||
return db.executeSql(`DELETE FROM plans WHERE id = ?`, [id]);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getAllPlans = async (): Promise<Plan[]> => {
|
|
||||||
const select = `SELECT * from plans`;
|
|
||||||
const [result] = await db.executeSql(select);
|
|
||||||
return result.rows.raw();
|
|
||||||
};
|
|
16
plan.ts
|
@ -1,5 +1,13 @@
|
||||||
export interface Plan {
|
import {Column, Entity, PrimaryGeneratedColumn} from 'typeorm'
|
||||||
id?: number;
|
|
||||||
days: string;
|
@Entity('plans')
|
||||||
workouts: string;
|
export class Plan {
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
id?: number
|
||||||
|
|
||||||
|
@Column('text')
|
||||||
|
days: string
|
||||||
|
|
||||||
|
@Column('text')
|
||||||
|
workouts: string
|
||||||
}
|
}
|
||||||
|
|
8
route.ts
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
197
set.service.ts
|
@ -1,197 +0,0 @@
|
||||||
import CountMany from './count-many';
|
|
||||||
import {db} from './db';
|
|
||||||
import Set from './set';
|
|
||||||
|
|
||||||
export const updateSet = async (value: Set) => {
|
|
||||||
const update = `
|
|
||||||
UPDATE sets
|
|
||||||
SET name = ?, reps = ?, weight = ?, unit = ?, image = ?
|
|
||||||
WHERE id = ?
|
|
||||||
`;
|
|
||||||
return db.executeSql(update, [
|
|
||||||
value.name,
|
|
||||||
value.reps,
|
|
||||||
value.weight,
|
|
||||||
value.unit,
|
|
||||||
value.image,
|
|
||||||
value.id,
|
|
||||||
]);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const addSets = async (columns: string, values: string) => {
|
|
||||||
console.log({columns, values});
|
|
||||||
const insert = `
|
|
||||||
INSERT INTO sets(${columns})
|
|
||||||
VALUES ${values}
|
|
||||||
`;
|
|
||||||
return db.executeSql(insert);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const addSet = async (value: Set) => {
|
|
||||||
const keys = Object.keys(value) as (keyof Set)[];
|
|
||||||
const questions = keys.map(() => '?').join(',');
|
|
||||||
const insert = `
|
|
||||||
INSERT INTO sets(${keys.join(',')},created)
|
|
||||||
VALUES (${questions},strftime('%Y-%m-%dT%H:%M:%S','now','localtime'))
|
|
||||||
`;
|
|
||||||
const values = keys.map(key => value[key]);
|
|
||||||
return db.executeSql(insert, values);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const deleteSets = async () => {
|
|
||||||
return db.executeSql(`DELETE FROM sets`);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const deleteSet = async (id: number) => {
|
|
||||||
return db.executeSql(`DELETE FROM sets WHERE id = ?`, [id]);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const deleteSetsBy = async (name: string) => {
|
|
||||||
return db.executeSql(`DELETE FROM sets WHERE name = ?`, [name]);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getAllSets = async (): Promise<Set[]> => {
|
|
||||||
const select = `SELECT * from sets`;
|
|
||||||
const [result] = await db.executeSql(select);
|
|
||||||
return result.rows.raw();
|
|
||||||
};
|
|
||||||
|
|
||||||
interface PageParams {
|
|
||||||
search: string;
|
|
||||||
limit: number;
|
|
||||||
offset: number;
|
|
||||||
format?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getSet = async (name: string): Promise<Set> => {
|
|
||||||
const select = `
|
|
||||||
SELECT *
|
|
||||||
FROM sets
|
|
||||||
WHERE name = ?
|
|
||||||
LIMIT 1
|
|
||||||
`;
|
|
||||||
const [result] = await db.executeSql(select, [name]);
|
|
||||||
return result.rows.item(0);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getSets = async ({
|
|
||||||
search,
|
|
||||||
limit,
|
|
||||||
offset,
|
|
||||||
format,
|
|
||||||
}: PageParams): Promise<Set[]> => {
|
|
||||||
const select = `
|
|
||||||
SELECT id, name, reps, weight, sets, minutes, seconds,
|
|
||||||
created, unit, image, steps
|
|
||||||
FROM sets
|
|
||||||
WHERE name LIKE ? AND NOT hidden
|
|
||||||
ORDER BY STRFTIME('%Y-%m-%d %H:%M', created) DESC
|
|
||||||
LIMIT ? OFFSET ?
|
|
||||||
`;
|
|
||||||
const [result] = await db.executeSql(select, [`%${search}%`, limit, offset]);
|
|
||||||
return result.rows.raw();
|
|
||||||
};
|
|
||||||
|
|
||||||
export const defaultSet: Set = {
|
|
||||||
name: '',
|
|
||||||
reps: 10,
|
|
||||||
weight: 20,
|
|
||||||
unit: 'kg',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const updateManySet = async ({
|
|
||||||
oldName,
|
|
||||||
newName,
|
|
||||||
minutes,
|
|
||||||
seconds,
|
|
||||||
sets,
|
|
||||||
steps,
|
|
||||||
}: {
|
|
||||||
oldName: string;
|
|
||||||
newName: string;
|
|
||||||
minutes: string;
|
|
||||||
seconds: string;
|
|
||||||
sets: string;
|
|
||||||
steps?: string;
|
|
||||||
}) => {
|
|
||||||
const update = `
|
|
||||||
UPDATE sets SET name = ?, minutes = ?, seconds = ?, sets = ?, steps = ?
|
|
||||||
WHERE name = ?
|
|
||||||
`;
|
|
||||||
return db.executeSql(update, [
|
|
||||||
newName,
|
|
||||||
minutes,
|
|
||||||
seconds,
|
|
||||||
sets,
|
|
||||||
steps,
|
|
||||||
oldName,
|
|
||||||
]);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const updateSetImage = async (name: string, image: string) => {
|
|
||||||
const update = `UPDATE sets SET image = ? WHERE name = ?`;
|
|
||||||
return db.executeSql(update, [image, name]);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getNames = async (): Promise<string[]> => {
|
|
||||||
const [result] = await db.executeSql('SELECT DISTINCT name FROM sets');
|
|
||||||
const values: {name: string}[] = result.rows.raw();
|
|
||||||
return values.map(value => value.name);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getToday = async (): Promise<Set | undefined> => {
|
|
||||||
const select = `
|
|
||||||
SELECT name, reps, weight, sets, minutes, seconds, unit, image FROM sets
|
|
||||||
WHERE NOT hidden
|
|
||||||
AND created LIKE strftime('%Y-%m-%d%%', 'now', 'localtime')
|
|
||||||
ORDER BY created DESC
|
|
||||||
LIMIT 1
|
|
||||||
`;
|
|
||||||
const [result] = await db.executeSql(select);
|
|
||||||
return result.rows.item(0);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const countToday = async (name: string): Promise<number> => {
|
|
||||||
const select = `
|
|
||||||
SELECT COUNT(*) as total FROM sets
|
|
||||||
WHERE created LIKE strftime('%Y-%m-%d%%', 'now', 'localtime')
|
|
||||||
AND name = ? AND NOT hidden
|
|
||||||
`;
|
|
||||||
const [result] = await db.executeSql(select, [name]);
|
|
||||||
return Number(result.rows.item(0)?.total);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const countMany = async (names: string[]): Promise<CountMany[]> => {
|
|
||||||
const questions = names.map(_ => '?').join(',');
|
|
||||||
console.log({questions, names});
|
|
||||||
const select = `
|
|
||||||
SELECT workouts.name, COUNT(sets.id) as total, workouts.sets
|
|
||||||
FROM (
|
|
||||||
SELECT distinct name, sets FROM sets
|
|
||||||
WHERE name IN (${questions})
|
|
||||||
) workouts
|
|
||||||
LEFT JOIN sets ON sets.name = workouts.name
|
|
||||||
AND sets.created LIKE STRFTIME('%Y-%m-%d%%', 'now', 'localtime')
|
|
||||||
AND NOT sets.hidden
|
|
||||||
GROUP BY workouts.name;
|
|
||||||
`;
|
|
||||||
const [result] = await db.executeSql(select, names);
|
|
||||||
return result.rows.raw();
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getDistinctSets = async ({
|
|
||||||
search,
|
|
||||||
limit,
|
|
||||||
offset,
|
|
||||||
}: PageParams): Promise<Set[]> => {
|
|
||||||
const select = `
|
|
||||||
SELECT name, image, sets, minutes, seconds, steps
|
|
||||||
FROM sets
|
|
||||||
WHERE sets.name LIKE ?
|
|
||||||
GROUP BY sets.name
|
|
||||||
ORDER BY sets.name
|
|
||||||
LIMIT ? OFFSET ?
|
|
||||||
`;
|
|
||||||
const [result] = await db.executeSql(select, [search, limit, offset]);
|
|
||||||
return result.rows.raw();
|
|
||||||
};
|
|
14
set.ts
|
@ -1,14 +0,0 @@
|
||||||
export default interface Set {
|
|
||||||
id?: number;
|
|
||||||
name: string;
|
|
||||||
reps: number;
|
|
||||||
weight: number;
|
|
||||||
sets?: number;
|
|
||||||
minutes?: number;
|
|
||||||
seconds?: number;
|
|
||||||
created?: string;
|
|
||||||
unit?: string;
|
|
||||||
hidden?: boolean;
|
|
||||||
image?: string;
|
|
||||||
steps?: string;
|
|
||||||
}
|
|
|
@ -1,23 +0,0 @@
|
||||||
import {db} from './db';
|
|
||||||
import Settings from './settings';
|
|
||||||
|
|
||||||
export const getSettings = async (): Promise<Settings> => {
|
|
||||||
const [result] = await db.executeSql(`SELECT * FROM settings LIMIT 1`);
|
|
||||||
return result.rows.item(0);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const updateSettings = async (value: Settings) => {
|
|
||||||
console.log(`${updateSettings.name}`, {value});
|
|
||||||
const keys = Object.keys(value) as (keyof Settings)[];
|
|
||||||
const sets = keys.map(key => `${key}=?`).join(',');
|
|
||||||
const update = `UPDATE settings SET ${sets}`;
|
|
||||||
const values = keys.map(key => value[key]);
|
|
||||||
return db.executeSql(update, values);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getNext = async (): Promise<string | undefined> => {
|
|
||||||
const [result] = await db.executeSql(
|
|
||||||
`SELECT nextAlarm FROM settings LIMIT 1`,
|
|
||||||
);
|
|
||||||
return result.rows.item(0)?.nextAlarm;
|
|
||||||
};
|
|
56
settings.ts
|
@ -1,15 +1,43 @@
|
||||||
export default interface Settings {
|
import {Column, Entity, PrimaryColumn} from 'typeorm'
|
||||||
alarm: number;
|
|
||||||
vibrate: number;
|
@Entity()
|
||||||
sound: string;
|
export default class Settings {
|
||||||
notify: number;
|
@PrimaryColumn('boolean')
|
||||||
images: number;
|
alarm: boolean
|
||||||
showUnit: number;
|
|
||||||
color: string;
|
@Column('boolean')
|
||||||
steps: number;
|
vibrate: boolean
|
||||||
date: string;
|
|
||||||
showDate: number;
|
@Column('text')
|
||||||
theme: 'system' | 'dark' | 'light';
|
sound: string
|
||||||
showSets: number;
|
|
||||||
noSound: number;
|
@Column('boolean')
|
||||||
|
notify: boolean
|
||||||
|
|
||||||
|
@Column('boolean')
|
||||||
|
images: boolean
|
||||||
|
|
||||||
|
@Column('boolean')
|
||||||
|
showUnit: boolean
|
||||||
|
|
||||||
|
@Column('text')
|
||||||
|
color: string
|
||||||
|
|
||||||
|
@Column('boolean')
|
||||||
|
steps: boolean
|
||||||
|
|
||||||
|
@Column('text')
|
||||||
|
date: string
|
||||||
|
|
||||||
|
@Column('boolean')
|
||||||
|
showDate: boolean
|
||||||
|
|
||||||
|
@Column('text')
|
||||||
|
theme: string
|
||||||
|
|
||||||
|
@Column('boolean')
|
||||||
|
showSets: boolean
|
||||||
|
|
||||||
|
@Column('boolean')
|
||||||
|
noSound: boolean
|
||||||
}
|
}
|
||||||
|
|
50
time.ts
|
@ -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 === 0 ? 0 : day - 1];
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
7
toast.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import {DeviceEventEmitter} from 'react-native'
|
||||||
|
|
||||||
|
export const TOAST = 'toast'
|
||||||
|
|
||||||
|
export function toast(value: string) {
|
||||||
|
DeviceEventEmitter.emit(TOAST, {value})
|
||||||
|
}
|
|
@ -5,7 +5,7 @@
|
||||||
"jsx": "react-native",
|
"jsx": "react-native",
|
||||||
"module": "CommonJS",
|
"module": "CommonJS",
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"types": ["react-native"],
|
"types": ["react-native", "jest"],
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
|
@ -13,8 +13,15 @@
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"strict": true,
|
"strict": false,
|
||||||
"skipLibCheck": true
|
"skipLibCheck": true,
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
|
"experimentalDecorators": true
|
||||||
},
|
},
|
||||||
"exclude": ["node_modules", "babel.config.js", "metro.config.js"]
|
"exclude": [
|
||||||
|
"node_modules",
|
||||||
|
"babel.config.js",
|
||||||
|
"metro.config.js",
|
||||||
|
"jest.config.js"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
14
use-dark.ts
|
@ -1,11 +1,11 @@
|
||||||
import {useColorScheme} from 'react-native';
|
import {useColorScheme} from 'react-native'
|
||||||
import {useSettings} from './use-settings';
|
import {useTheme} from './use-theme'
|
||||||
|
|
||||||
export default function useDark() {
|
export default function useDark() {
|
||||||
const dark = useColorScheme() === 'dark';
|
const dark = useColorScheme() === 'dark'
|
||||||
const {settings} = useSettings();
|
const {theme} = useTheme()
|
||||||
|
|
||||||
if (settings.theme === 'dark') return true;
|
if (theme === 'dark') return true
|
||||||
if (settings.theme === 'light') return false;
|
if (theme === 'light') return false
|
||||||
return dark;
|
return dark
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,27 +0,0 @@
|
||||||
import React, {useContext} from 'react';
|
|
||||||
import Settings from './settings';
|
|
||||||
|
|
||||||
export const SettingsContext = React.createContext<{
|
|
||||||
settings: Settings;
|
|
||||||
setSettings: (value: Settings) => void;
|
|
||||||
}>({
|
|
||||||
settings: {
|
|
||||||
alarm: 0,
|
|
||||||
color: '',
|
|
||||||
date: '',
|
|
||||||
images: 1,
|
|
||||||
notify: 0,
|
|
||||||
showDate: 0,
|
|
||||||
showSets: 1,
|
|
||||||
showUnit: 1,
|
|
||||||
sound: '',
|
|
||||||
steps: 0,
|
|
||||||
theme: 'system',
|
|
||||||
vibrate: 1,
|
|
||||||
},
|
|
||||||
setSettings: () => null,
|
|
||||||
});
|
|
||||||
|
|
||||||
export function useSettings() {
|
|
||||||
return useContext(SettingsContext);
|
|
||||||
}
|
|