Run prettier

Something happened with the deno formatter,
I can't remember what! Hahahahahaahahaha
This commit is contained in:
Brandon Presley 2023-08-12 15:22:50 +12:00
parent 44283fc990
commit f778426aba
57 changed files with 1609 additions and 1628 deletions

104
App.tsx
View File

@ -2,21 +2,21 @@ 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 React, { useEffect, useMemo, useState } from "react";
import { DeviceEventEmitter, useColorScheme } from 'react-native' import { DeviceEventEmitter, useColorScheme } from "react-native";
import { import {
MD3DarkTheme as PaperDarkTheme, MD3DarkTheme as PaperDarkTheme,
MD3LightTheme as PaperDefaultTheme, MD3LightTheme as PaperDefaultTheme,
Provider as PaperProvider, Provider as PaperProvider,
Snackbar, Snackbar,
} from 'react-native-paper' } from "react-native-paper";
import MaterialIcon from 'react-native-vector-icons/MaterialIcons' import MaterialIcon from "react-native-vector-icons/MaterialIcons";
import { AppDataSource } from './data-source' import { AppDataSource } from "./data-source";
import { settingsRepo } from './db' import { settingsRepo } from "./db";
import Routes from './Routes' import Routes from "./Routes";
import { TOAST } from './toast' import { TOAST } from "./toast";
import { ThemeContext } from './use-theme' import { ThemeContext } from "./use-theme";
export const CombinedDefaultTheme = { export const CombinedDefaultTheme = {
...NavigationDefaultTheme, ...NavigationDefaultTheme,
@ -25,7 +25,7 @@ export const CombinedDefaultTheme = {
...NavigationDefaultTheme.colors, ...NavigationDefaultTheme.colors,
...PaperDefaultTheme.colors, ...PaperDefaultTheme.colors,
}, },
} };
export const CombinedDarkTheme = { export const CombinedDarkTheme = {
...NavigationDarkTheme, ...NavigationDarkTheme,
@ -34,58 +34,58 @@ export const CombinedDarkTheme = {
...NavigationDarkTheme.colors, ...NavigationDarkTheme.colors,
...PaperDarkTheme.colors, ...PaperDarkTheme.colors,
}, },
} };
const App = () => { const App = () => {
const phoneTheme = useColorScheme() const phoneTheme = useColorScheme();
const [initialized, setInitialized] = useState(false) const [initialized, setInitialized] = useState(false);
const [snackbar, setSnackbar] = useState('') const [snackbar, setSnackbar] = useState("");
const [appTheme, setAppTheme] = useState('system') const [appTheme, setAppTheme] = useState("system");
const [lightColor, setLightColor] = useState<string>( const [lightColor, setLightColor] = useState<string>(
CombinedDefaultTheme.colors.primary, CombinedDefaultTheme.colors.primary
) );
const [darkColor, setDarkColor] = useState<string>( const [darkColor, setDarkColor] = useState<string>(
CombinedDarkTheme.colors.primary, CombinedDarkTheme.colors.primary
) );
useEffect(() => { useEffect(() => {
;(async () => { (async () => {
if (!AppDataSource.isInitialized) await AppDataSource.initialize() if (!AppDataSource.isInitialized) await AppDataSource.initialize();
const settings = await settingsRepo.findOne({ where: {} }) const settings = await settingsRepo.findOne({ where: {} });
setAppTheme(settings.theme) setAppTheme(settings.theme);
if (settings.lightColor) setLightColor(settings.lightColor) if (settings.lightColor) setLightColor(settings.lightColor);
if (settings.darkColor) setDarkColor(settings.darkColor) if (settings.darkColor) setDarkColor(settings.darkColor);
setInitialized(true) setInitialized(true);
})() })();
const description = DeviceEventEmitter.addListener( const description = DeviceEventEmitter.addListener(
TOAST, TOAST,
({ value }: { value: string }) => { ({ value }: { value: string }) => {
setSnackbar(value) setSnackbar(value);
}, }
) );
return description.remove return description.remove;
}, []) }, []);
const paperTheme = useMemo(() => { const paperTheme = useMemo(() => {
const darkTheme = lightColor const darkTheme = lightColor
? { ? {
...CombinedDarkTheme, ...CombinedDarkTheme,
colors: { ...CombinedDarkTheme.colors, primary: darkColor }, colors: { ...CombinedDarkTheme.colors, primary: darkColor },
} }
: CombinedDarkTheme : CombinedDarkTheme;
const lightTheme = lightColor const lightTheme = lightColor
? { ? {
...CombinedDefaultTheme, ...CombinedDefaultTheme,
colors: { ...CombinedDefaultTheme.colors, primary: lightColor }, colors: { ...CombinedDefaultTheme.colors, primary: lightColor },
} }
: CombinedDefaultTheme : CombinedDefaultTheme;
let value = phoneTheme === 'dark' ? darkTheme : lightTheme let value = phoneTheme === "dark" ? darkTheme : lightTheme;
if (appTheme === 'dark') value = darkTheme if (appTheme === "dark") value = darkTheme;
else if (appTheme === 'light') value = lightTheme else if (appTheme === "light") value = lightTheme;
return value return value;
}, [phoneTheme, appTheme, lightColor, darkColor]) }, [phoneTheme, appTheme, lightColor, darkColor]);
return ( return (
<PaperProvider <PaperProvider
@ -111,18 +111,18 @@ const App = () => {
<Snackbar <Snackbar
duration={3000} duration={3000}
onDismiss={() => setSnackbar('')} onDismiss={() => setSnackbar("")}
visible={!!snackbar} visible={!!snackbar}
action={{ action={{
label: 'Close', label: "Close",
onPress: () => setSnackbar(''), onPress: () => setSnackbar(""),
textColor: paperTheme.colors.background, textColor: paperTheme.colors.background,
}} }}
> >
{snackbar} {snackbar}
</Snackbar> </Snackbar>
</PaperProvider> </PaperProvider>
) );
} };
export default App export default App;

View File

@ -1,31 +1,31 @@
import { ComponentProps, useMemo } from 'react' import { ComponentProps, useMemo } from "react";
import { FAB, useTheme } from 'react-native-paper' import { FAB, useTheme } from "react-native-paper";
import { CombinedDarkTheme, CombinedDefaultTheme } from './App' import { CombinedDarkTheme, CombinedDefaultTheme } from "./App";
import { lightColors } from './colors' import { lightColors } from "./colors";
export default function AppFab(props: Partial<ComponentProps<typeof FAB>>) { export default function AppFab(props: Partial<ComponentProps<typeof FAB>>) {
const { colors } = useTheme() const { colors } = useTheme();
const fabColor = useMemo( const fabColor = useMemo(
() => () =>
lightColors.map((color) => color.hex).includes(colors.primary) lightColors.map((color) => color.hex).includes(colors.primary)
? CombinedDarkTheme.colors.background ? CombinedDarkTheme.colors.background
: CombinedDefaultTheme.colors.background, : CombinedDefaultTheme.colors.background,
[colors.primary], [colors.primary]
) );
return ( return (
<FAB <FAB
icon='add' icon="add"
testID='add' testID="add"
color={fabColor} color={fabColor}
style={{ style={{
position: 'absolute', position: "absolute",
right: 20, right: 20,
bottom: 20, bottom: 20,
backgroundColor: colors.primary, backgroundColor: colors.primary,
}} }}
{...props} {...props}
/> />
) );
} }

View File

@ -1,26 +1,26 @@
import React, { ComponentProps, Ref } from 'react' import React, { 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";
function AppInput( function AppInput(
props: Partial<ComponentProps<typeof TextInput>> & { props: Partial<ComponentProps<typeof TextInput>> & {
innerRef?: Ref<any> innerRef?: Ref<any>;
}, }
) { ) {
const dark = useDark() const dark = useDark();
return ( return (
<TextInput <TextInput
selectionColor={dark ? '#2A2A2A' : CombinedDefaultTheme.colors.border} selectionColor={dark ? "#2A2A2A" : CombinedDefaultTheme.colors.border}
style={{ marginBottom: MARGIN, minWidth: 100 }} style={{ marginBottom: MARGIN, minWidth: 100 }}
selectTextOnFocus selectTextOnFocus
ref={props.innerRef} ref={props.innerRef}
blurOnSubmit={false} blurOnSubmit={false}
{...props} {...props}
/> />
) );
} }
export default React.memo(AppInput) export default React.memo(AppInput);

View File

@ -1,11 +1,11 @@
import { useTheme } from '@react-navigation/native' import { useTheme } from "@react-navigation/native";
import * as shape from 'd3-shape' import * as shape from "d3-shape";
import { View } from 'react-native' import { View } from "react-native";
import { Grid, LineChart, XAxis, YAxis } from 'react-native-svg-charts' import { Grid, LineChart, XAxis, YAxis } from "react-native-svg-charts";
import { CombinedDarkTheme, CombinedDefaultTheme } from './App' import { CombinedDarkTheme, CombinedDefaultTheme } from "./App";
import { MARGIN, PADDING } from './constants' import { MARGIN, PADDING } from "./constants";
import GymSet from './gym-set' import GymSet from "./gym-set";
import useDark from './use-dark' import useDark from "./use-dark";
export default function Chart({ export default function Chart({
yData, yData,
@ -13,21 +13,21 @@ export default function Chart({
xData, xData,
yFormat, yFormat,
}: { }: {
yData: number[] yData: number[];
xData: GymSet[] xData: GymSet[];
xFormat: (value: any, index: number) => string xFormat: (value: any, index: number) => string;
yFormat: (value: any) => string yFormat: (value: any) => string;
}) { }) {
const { colors } = useTheme() 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 (
<> <>
@ -35,7 +35,7 @@ export default function Chart({
style={{ style={{
height: 300, height: 300,
padding: PADDING, padding: PADDING,
flexDirection: 'row', flexDirection: "row",
}} }}
> >
<YAxis <YAxis
@ -66,5 +66,5 @@ export default function Chart({
</View> </View>
</View> </View>
</> </>
) );
} }

View File

@ -1,4 +1,4 @@
import { Button, Dialog, Portal, Text } from 'react-native-paper' import { Button, Dialog, Portal, Text } from "react-native-paper";
export default function ConfirmDialog({ export default function ConfirmDialog({
title, title,
@ -8,17 +8,17 @@ export default function ConfirmDialog({
setShow, setShow,
onCancel, onCancel,
}: { }: {
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;
onCancel?: () => void onCancel?: () => void;
}) { }) {
const cancel = () => { const cancel = () => {
setShow(false) setShow(false);
onCancel && onCancel() onCancel && onCancel();
} };
return ( return (
<Portal> <Portal>
@ -33,5 +33,5 @@ export default function ConfirmDialog({
</Dialog.Actions> </Dialog.Actions>
</Dialog> </Dialog>
</Portal> </Portal>
) );
} }

View File

@ -1,25 +1,22 @@
import { DrawerNavigationProp } from '@react-navigation/drawer' import { DrawerNavigationProp } from "@react-navigation/drawer";
import { useNavigation } from '@react-navigation/native' import { useNavigation } from "@react-navigation/native";
import { Appbar, IconButton } from 'react-native-paper' import { Appbar, IconButton } from "react-native-paper";
import { DrawerParamList } from './drawer-param-list' import { DrawerParamList } from "./drawer-param-list";
export default function DrawerHeader({ export default function DrawerHeader({
name, name,
children, children,
}: { }: {
name: string name: string;
children?: JSX.Element | JSX.Element[] children?: JSX.Element | JSX.Element[];
}) { }) {
const navigation = useNavigation<DrawerNavigationProp<DrawerParamList>>() const navigation = useNavigation<DrawerNavigationProp<DrawerParamList>>();
return ( return (
<Appbar.Header> <Appbar.Header>
<IconButton <IconButton icon="menu" onPress={navigation.openDrawer} />
icon='menu'
onPress={navigation.openDrawer}
/>
<Appbar.Content title={name} /> <Appbar.Content title={name} />
{children} {children}
</Appbar.Header> </Appbar.Header>
) );
} }

View File

@ -3,92 +3,94 @@ import {
RouteProp, RouteProp,
useNavigation, useNavigation,
useRoute, useRoute,
} from '@react-navigation/native' } from "@react-navigation/native";
import { useCallback, useEffect, useState } from 'react' import { useCallback, useEffect, useState } from "react";
import { ScrollView, StyleSheet, View } from 'react-native' import { ScrollView, StyleSheet, View } from "react-native";
import { Button, IconButton, Text } from 'react-native-paper' import { Button, IconButton, Text } from "react-native-paper";
import { MARGIN, PADDING } from './constants' import { MARGIN, PADDING } from "./constants";
import { planRepo, setRepo } from './db' import { planRepo, setRepo } from "./db";
import { defaultSet } from './gym-set' import { defaultSet } from "./gym-set";
import { PlanPageParams } from './plan-page-params' import { PlanPageParams } from "./plan-page-params";
import StackHeader from './StackHeader' import StackHeader from "./StackHeader";
import Switch from './Switch' import Switch from "./Switch";
import { DAYS } from './time' import { DAYS } from "./time";
export default function EditPlan() { export default function EditPlan() {
const { params } = useRoute<RouteProp<PlanPageParams, 'EditPlan'>>() const { params } = useRoute<RouteProp<PlanPageParams, "EditPlan">>();
const { plan } = params const { plan } = params;
const [days, setDays] = useState<string[]>( const [days, setDays] = useState<string[]>(
plan.days ? plan.days.split(',') : [], plan.days ? plan.days.split(",") : []
) );
const [workouts, setWorkouts] = useState<string[]>( const [workouts, setWorkouts] = useState<string[]>(
plan.workouts ? plan.workouts.split(',') : [], plan.workouts ? plan.workouts.split(",") : []
) );
const [names, setNames] = useState<string[]>([]) const [names, setNames] = useState<string[]>([]);
const navigation = useNavigation<NavigationProp<PlanPageParams>>() const navigation = useNavigation<NavigationProp<PlanPageParams>>();
useEffect(() => { useEffect(() => {
setRepo setRepo
.createQueryBuilder() .createQueryBuilder()
.select('name') .select("name")
.distinct(true) .distinct(true)
.orderBy('name') .orderBy("name")
.getRawMany() .getRawMany()
.then((values) => { .then((values) => {
console.log(EditPlan.name, { values }) console.log(EditPlan.name, { values });
setNames(values.map((value) => value.name)) setNames(values.map((value) => value.name));
}) });
}, []) }, []);
const save = useCallback(async () => { const save = useCallback(async () => {
console.log(`${EditPlan.name}.save`, { days, workouts, plan }) console.log(`${EditPlan.name}.save`, { days, workouts, plan });
if (!days || !workouts) return if (!days || !workouts) return;
const newWorkouts = workouts.filter((workout) => workout).join(',') const newWorkouts = workouts.filter((workout) => workout).join(",");
const newDays = days.filter((day) => day).join(',') const newDays = days.filter((day) => day).join(",");
await planRepo.save({ days: newDays, workouts: newWorkouts, id: plan.id }) await planRepo.save({ days: newDays, workouts: newWorkouts, id: plan.id });
}, [days, workouts, plan]) }, [days, workouts, plan]);
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 (
<> <>
<StackHeader <StackHeader
title={typeof plan.id === 'number' ? 'Edit plan' : 'Add plan'} title={typeof plan.id === "number" ? "Edit plan" : "Add plan"}
> >
{typeof plan.id === 'number' && ( {typeof plan.id === "number" && (
<IconButton <IconButton
onPress={async () => { onPress={async () => {
await save() await save();
const newPlan = await planRepo.findOne({ where: { id: plan.id } }) const newPlan = await planRepo.findOne({
where: { id: plan.id },
});
let first = await setRepo.findOne({ let first = await setRepo.findOne({
where: { name: workouts[0] }, where: { name: workouts[0] },
order: { created: 'desc' }, order: { created: "desc" },
}) });
if (!first) first = { ...defaultSet, name: workouts[0] } if (!first) first = { ...defaultSet, name: workouts[0] };
delete first.id delete first.id;
navigation.navigate('StartPlan', { plan: newPlan, first }) navigation.navigate("StartPlan", { plan: newPlan, first });
}} }}
icon='play-arrow' icon="play-arrow"
/> />
)} )}
</StackHeader> </StackHeader>
@ -104,39 +106,37 @@ export default function EditPlan() {
/> />
))} ))}
<Text style={[styles.title, { marginTop: MARGIN }]}>Workouts</Text> <Text style={[styles.title, { marginTop: MARGIN }]}>Workouts</Text>
{names.length === 0 {names.length === 0 ? (
? ( <View>
<View> <Text>No workouts found.</Text>
<Text>No workouts found.</Text> </View>
</View> ) : (
) names.map((name) => (
: ( <Switch
names.map((name) => ( key={name}
<Switch onChange={(value) => toggleWorkout(value, name)}
key={name} value={workouts.includes(name)}
onChange={(value) => toggleWorkout(value, name)} title={name}
value={workouts.includes(name)} />
title={name} ))
/> )}
))
)}
</ScrollView> </ScrollView>
<Button <Button
disabled={workouts.length === 0 && days.length === 0} disabled={workouts.length === 0 && days.length === 0}
style={styles.button} style={styles.button}
mode='outlined' mode="outlined"
icon='save' icon="save"
onPress={async () => { onPress={async () => {
await save() await save();
navigation.navigate('PlanList') navigation.navigate("PlanList");
}} }}
> >
Save Save
</Button> </Button>
</View> </View>
</> </>
) );
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
@ -145,4 +145,4 @@ const styles = StyleSheet.create({
marginBottom: MARGIN, marginBottom: MARGIN,
}, },
button: {}, button: {},
}) });

View File

@ -3,78 +3,78 @@ import {
useFocusEffect, useFocusEffect,
useNavigation, useNavigation,
useRoute, useRoute,
} from '@react-navigation/native' } from "@react-navigation/native";
import { useCallback, useState } from 'react' import { useCallback, useState } from "react";
import { View } from 'react-native' import { 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 { In } from 'typeorm' import { In } from "typeorm";
import AppInput from './AppInput' import AppInput from "./AppInput";
import ConfirmDialog from './ConfirmDialog' import ConfirmDialog from "./ConfirmDialog";
import { MARGIN, PADDING } from './constants' import { MARGIN, PADDING } from "./constants";
import { setRepo, settingsRepo } from './db' import { setRepo, settingsRepo } from "./db";
import GymSet from './gym-set' import GymSet from "./gym-set";
import { HomePageParams } from './home-page-params' import { HomePageParams } from "./home-page-params";
import Settings from './settings' import Settings from "./settings";
import StackHeader from './StackHeader' import StackHeader from "./StackHeader";
export default function EditSets() { export default function EditSets() {
const { params } = useRoute<RouteProp<HomePageParams, 'EditSets'>>() const { params } = useRoute<RouteProp<HomePageParams, "EditSets">>();
const { ids } = params const { ids } = params;
const navigation = useNavigation() const navigation = useNavigation();
const [settings, setSettings] = useState<Settings>({} as Settings) const [settings, setSettings] = useState<Settings>({} as Settings);
const [name, setName] = useState('') const [name, setName] = useState("");
const [reps, setReps] = useState('') const [reps, setReps] = useState("");
const [weight, setWeight] = useState('') const [weight, setWeight] = useState("");
const [newImage, setNewImage] = useState('') const [newImage, setNewImage] = useState("");
const [unit, setUnit] = useState('') const [unit, setUnit] = useState("");
const [showRemove, setShowRemove] = useState(false) const [showRemove, setShowRemove] = useState(false);
const [names, setNames] = useState('') const [names, setNames] = useState("");
const [oldReps, setOldReps] = useState('') const [oldReps, setOldReps] = useState("");
const [weights, setWeights] = useState('') const [weights, setWeights] = useState("");
const [units, setUnits] = useState('') const [units, setUnits] = useState("");
const [selection, setSelection] = useState({ const [selection, setSelection] = useState({
start: 0, start: 0,
end: 1, end: 1,
}) });
useFocusEffect( useFocusEffect(
useCallback(() => { useCallback(() => {
settingsRepo.findOne({ where: {} }).then(setSettings) settingsRepo.findOne({ where: {} }).then(setSettings);
setRepo.find({ where: { id: In(ids) } }).then((sets) => { setRepo.find({ where: { id: In(ids) } }).then((sets) => {
setNames(sets.map((set) => set.name).join(', ')) setNames(sets.map((set) => set.name).join(", "));
setOldReps(sets.map((set) => set.reps).join(', ')) setOldReps(sets.map((set) => set.reps).join(", "));
setWeights(sets.map((set) => set.weight).join(', ')) setWeights(sets.map((set) => set.weight).join(", "));
setUnits(sets.map((set) => set.unit).join(', ')) setUnits(sets.map((set) => set.unit).join(", "));
}) });
}, [ids]), }, [ids])
) );
const handleSubmit = async () => { const handleSubmit = async () => {
console.log(`${EditSets.name}.handleSubmit:`, { uri: newImage, name }) console.log(`${EditSets.name}.handleSubmit:`, { uri: newImage, name });
const update: Partial<GymSet> = {} const update: Partial<GymSet> = {};
if (name) update.name = name if (name) update.name = name;
if (reps) update.reps = Number(reps) if (reps) update.reps = Number(reps);
if (weight) update.weight = Number(weight) if (weight) update.weight = Number(weight);
if (unit) update.unit = unit if (unit) update.unit = unit;
if (newImage) update.image = newImage if (newImage) update.image = newImage;
if (Object.keys(update).length > 0) await setRepo.update(ids, update) if (Object.keys(update).length > 0) await setRepo.update(ids, update);
navigation.goBack() navigation.goBack();
} };
const changeImage = useCallback(async () => { const changeImage = useCallback(async () => {
const { fileCopyUri } = await DocumentPicker.pickSingle({ const { fileCopyUri } = await DocumentPicker.pickSingle({
type: DocumentPicker.types.images, type: DocumentPicker.types.images,
copyTo: 'documentDirectory', copyTo: "documentDirectory",
}) });
if (fileCopyUri) setNewImage(fileCopyUri) if (fileCopyUri) setNewImage(fileCopyUri);
}, []) }, []);
const handleRemove = useCallback(async () => { const handleRemove = useCallback(async () => {
setNewImage('') setNewImage("");
setShowRemove(false) setShowRemove(false);
}, []) }, []);
return ( return (
<> <>
@ -91,7 +91,7 @@ export default function EditSets() {
<AppInput <AppInput
label={`Reps: ${oldReps}`} label={`Reps: ${oldReps}`}
keyboardType='numeric' keyboardType="numeric"
value={reps} value={reps}
onChangeText={setReps} onChangeText={setReps}
selection={selection} selection={selection}
@ -101,7 +101,7 @@ export default function EditSets() {
<AppInput <AppInput
label={`Weights: ${weights}`} label={`Weights: ${weights}`}
keyboardType='numeric' keyboardType="numeric"
value={weight} value={weight}
onChangeText={setWeight} onChangeText={setWeight}
onSubmitEditing={handleSubmit} onSubmitEditing={handleSubmit}
@ -109,7 +109,7 @@ export default function EditSets() {
{settings.showUnit && ( {settings.showUnit && (
<AppInput <AppInput
autoCapitalize='none' autoCapitalize="none"
label={`Units: ${units}`} label={`Units: ${units}`}
value={unit} value={unit}
onChangeText={setUnit} onChangeText={setUnit}
@ -126,7 +126,7 @@ export default function EditSets() {
</TouchableRipple> </TouchableRipple>
)} )}
<ConfirmDialog <ConfirmDialog
title='Remove image' title="Remove image"
onOk={handleRemove} onOk={handleRemove}
show={showRemove} show={showRemove}
setShow={setShowRemove} setShow={setShowRemove}
@ -138,7 +138,7 @@ export default function EditSets() {
<Button <Button
style={{ marginBottom: MARGIN }} style={{ marginBottom: MARGIN }}
onPress={changeImage} onPress={changeImage}
icon='add-photo-alternate' icon="add-photo-alternate"
> >
Image Image
</Button> </Button>
@ -146,13 +146,13 @@ export default function EditSets() {
</View> </View>
<Button <Button
mode='outlined' mode="outlined"
icon='save' icon="save"
style={{ margin: MARGIN }} style={{ margin: MARGIN }}
onPress={handleSubmit} onPress={handleSubmit}
> >
Save Save
</Button> </Button>
</> </>
) );
} }

View File

@ -3,46 +3,46 @@ import {
useFocusEffect, useFocusEffect,
useNavigation, useNavigation,
useRoute, useRoute,
} from '@react-navigation/native' } from "@react-navigation/native";
import { useCallback, useRef, useState } from 'react' import { useCallback, useRef, useState } from "react";
import { ScrollView, TextInput, View } from 'react-native' import { ScrollView, TextInput, View } from "react-native";
import DocumentPicker from 'react-native-document-picker' import DocumentPicker from "react-native-document-picker";
import { Button, Card, TouchableRipple } from 'react-native-paper' import { Button, Card, TouchableRipple } from "react-native-paper";
import AppInput from './AppInput' import AppInput from "./AppInput";
import ConfirmDialog from './ConfirmDialog' import ConfirmDialog from "./ConfirmDialog";
import { MARGIN, PADDING } from './constants' import { MARGIN, PADDING } from "./constants";
import { getNow, planRepo, setRepo, settingsRepo } from './db' import { getNow, planRepo, setRepo, settingsRepo } from "./db";
import { defaultSet } from './gym-set' import { defaultSet } from "./gym-set";
import Settings from './settings' import Settings from "./settings";
import StackHeader from './StackHeader' import StackHeader from "./StackHeader";
import { WorkoutsPageParams } from './WorkoutsPage' import { WorkoutsPageParams } from "./WorkoutsPage";
export default function EditWorkout() { export default function EditWorkout() {
const { params } = useRoute<RouteProp<WorkoutsPageParams, 'EditWorkout'>>() const { params } = useRoute<RouteProp<WorkoutsPageParams, "EditWorkout">>();
const [removeImage, setRemoveImage] = useState(false) const [removeImage, setRemoveImage] = useState(false);
const [showRemove, setShowRemove] = useState(false) const [showRemove, setShowRemove] = useState(false);
const [name, setName] = useState(params.value.name) const [name, setName] = useState(params.value.name);
const [steps, setSteps] = useState(params.value.steps) const [steps, setSteps] = useState(params.value.steps);
const [uri, setUri] = useState(params.value.image) const [uri, setUri] = useState(params.value.image);
const [minutes, setMinutes] = useState( const [minutes, setMinutes] = useState(
params.value.minutes?.toString() ?? '3', params.value.minutes?.toString() ?? "3"
) );
const [seconds, setSeconds] = useState( const [seconds, setSeconds] = useState(
params.value.seconds?.toString() ?? '30', params.value.seconds?.toString() ?? "30"
) );
const [sets, setSets] = useState(params.value.sets?.toString() ?? '3') const [sets, setSets] = useState(params.value.sets?.toString() ?? "3");
const 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, setSettings] = useState<Settings>();
useFocusEffect( useFocusEffect(
useCallback(() => { useCallback(() => {
settingsRepo.findOne({ where: {} }).then(setSettings) settingsRepo.findOne({ where: {} }).then(setSettings);
}, []), }, [])
) );
const update = async () => { const update = async () => {
await setRepo.update( await setRepo.update(
@ -53,20 +53,20 @@ export default function EditWorkout() {
minutes: +minutes, minutes: +minutes,
seconds: +seconds, seconds: +seconds,
steps, steps,
image: removeImage ? '' : uri, image: removeImage ? "" : uri,
}, }
) );
await planRepo.query( await planRepo.query(
`UPDATE plans `UPDATE plans
SET workouts = REPLACE(workouts, $1, $2) SET workouts = REPLACE(workouts, $1, $2)
WHERE workouts LIKE $3`, WHERE workouts LIKE $3`,
[params.value.name, name, `%${params.value.name}%`], [params.value.name, name, `%${params.value.name}%`]
) );
navigation.goBack() navigation.goBack();
} };
const add = async () => { const add = async () => {
const now = await getNow() const now = await getNow();
await setRepo.save({ await setRepo.save({
...defaultSet, ...defaultSet,
name, name,
@ -77,42 +77,42 @@ export default function EditWorkout() {
sets: sets ? +sets : 3, sets: sets ? +sets : 3,
steps, steps,
created: now, created: now,
}) });
navigation.goBack() navigation.goBack();
} };
const save = async () => { const save = async () => {
if (params.value.name) return update() if (params.value.name) return update();
return add() return add();
} };
const changeImage = useCallback(async () => { const changeImage = useCallback(async () => {
const { fileCopyUri } = await DocumentPicker.pickSingle({ const { fileCopyUri } = await DocumentPicker.pickSingle({
type: DocumentPicker.types.images, type: DocumentPicker.types.images,
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 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 (
<> <>
<StackHeader title={params.value.name ? 'Edit workout' : 'Add workout'} /> <StackHeader title={params.value.name ? "Edit workout" : "Add workout"} />
<View style={{ padding: PADDING, flex: 1 }}> <View style={{ padding: PADDING, flex: 1 }}>
<ScrollView style={{ flex: 1 }}> <ScrollView style={{ flex: 1 }}>
<AppInput <AppInput
autoFocus autoFocus
label='Name' label="Name"
value={name} value={name}
onChangeText={setName} onChangeText={setName}
onSubmitEditing={submitName} onSubmitEditing={submitName}
@ -123,7 +123,7 @@ export default function EditWorkout() {
selectTextOnFocus={false} selectTextOnFocus={false}
value={steps} value={steps}
onChangeText={setSteps} onChangeText={setSteps}
label='Steps' label="Steps"
multiline multiline
onSubmitEditing={() => setsRef.current?.focus()} onSubmitEditing={() => setsRef.current?.focus()}
/> />
@ -132,8 +132,8 @@ export default function EditWorkout() {
innerRef={setsRef} innerRef={setsRef}
value={sets} value={sets}
onChangeText={setSets} onChangeText={setSets}
label='Sets per workout' label="Sets per workout"
keyboardType='numeric' keyboardType="numeric"
onSubmitEditing={() => minutesRef.current?.focus()} onSubmitEditing={() => minutesRef.current?.focus()}
/> />
{settings?.alarm && ( {settings?.alarm && (
@ -143,15 +143,15 @@ export default function EditWorkout() {
onSubmitEditing={() => secondsRef.current?.focus()} onSubmitEditing={() => secondsRef.current?.focus()}
value={minutes} value={minutes}
onChangeText={setMinutes} onChangeText={setMinutes}
label='Rest minutes' label="Rest minutes"
keyboardType='numeric' keyboardType="numeric"
/> />
<AppInput <AppInput
innerRef={secondsRef} innerRef={secondsRef}
value={seconds} value={seconds}
onChangeText={setSeconds} onChangeText={setSeconds}
label='Rest seconds' label="Rest seconds"
keyboardType='numeric' keyboardType="numeric"
blurOnSubmit blurOnSubmit
/> />
</> </>
@ -169,17 +169,17 @@ export default function EditWorkout() {
<Button <Button
style={{ marginBottom: MARGIN }} style={{ marginBottom: MARGIN }}
onPress={changeImage} onPress={changeImage}
icon='add-photo-alternate' icon="add-photo-alternate"
> >
Image Image
</Button> </Button>
)} )}
</ScrollView> </ScrollView>
<Button disabled={!name} mode='outlined' icon='save' onPress={save}> <Button disabled={!name} mode="outlined" icon="save" onPress={save}>
Save Save
</Button> </Button>
<ConfirmDialog <ConfirmDialog
title='Remove image' title="Remove image"
onOk={handleRemove} onOk={handleRemove}
show={showRemove} show={showRemove}
setShow={setShowRemove} setShow={setShowRemove}
@ -188,5 +188,5 @@ export default function EditWorkout() {
</ConfirmDialog> </ConfirmDialog>
</View> </View>
</> </>
) );
} }

View File

@ -1,23 +1,23 @@
import { createStackNavigator } from '@react-navigation/stack' import { createStackNavigator } from "@react-navigation/stack";
import GraphsList from './GraphsList' import GraphsList from "./GraphsList";
import GymSet from './gym-set' import GymSet from "./gym-set";
import ViewGraph from './ViewGraph' import ViewGraph from "./ViewGraph";
const Stack = createStackNavigator<GraphsPageParams>() const Stack = createStackNavigator<GraphsPageParams>();
export type GraphsPageParams = { export type GraphsPageParams = {
GraphsList: {} GraphsList: {};
ViewGraph: { ViewGraph: {
best: GymSet best: GymSet;
} };
} };
export default function GraphsPage() { export default function GraphsPage() {
return ( return (
<Stack.Navigator <Stack.Navigator
screenOptions={{ headerShown: false, animationEnabled: false }} screenOptions={{ headerShown: false, animationEnabled: false }}
> >
<Stack.Screen name='GraphsList' component={GraphsList} /> <Stack.Screen name="GraphsList" component={GraphsList} />
<Stack.Screen name='ViewGraph' component={ViewGraph} /> <Stack.Screen name="ViewGraph" component={ViewGraph} />
</Stack.Navigator> </Stack.Navigator>
) );
} }

View File

@ -1,19 +1,19 @@
import { createStackNavigator } from '@react-navigation/stack' import { createStackNavigator } from "@react-navigation/stack";
import EditSet from './EditSet' import EditSet from "./EditSet";
import EditSets from './EditSets' import EditSets from "./EditSets";
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 (
<Stack.Navigator <Stack.Navigator
screenOptions={{ headerShown: false, animationEnabled: false }} screenOptions={{ headerShown: false, animationEnabled: false }}
> >
<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.Screen name='EditSets' component={EditSets} /> <Stack.Screen name="EditSets" component={EditSets} />
</Stack.Navigator> </Stack.Navigator>
) );
} }

View File

@ -1,6 +1,6 @@
import { useState } from 'react' import { useState } from "react";
import { Divider, IconButton, Menu } from 'react-native-paper' import { Divider, IconButton, Menu } from "react-native-paper";
import ConfirmDialog from './ConfirmDialog' import ConfirmDialog from "./ConfirmDialog";
export default function ListMenu({ export default function ListMenu({
onEdit, onEdit,
@ -10,88 +10,85 @@ export default function ListMenu({
onSelect, onSelect,
ids, ids,
}: { }: {
onEdit: () => void onEdit: () => void;
onCopy: () => void onCopy: () => void;
onClear: () => void onClear: () => void;
onDelete: () => void onDelete: () => void;
onSelect: () => void onSelect: () => void;
ids?: number[] ids?: number[];
}) { }) {
const [showMenu, setShowMenu] = useState(false) const [showMenu, setShowMenu] = useState(false);
const [showRemove, setShowRemove] = useState(false) const [showRemove, setShowRemove] = useState(false);
const edit = () => { const edit = () => {
setShowMenu(false) setShowMenu(false);
onEdit() onEdit();
} };
const copy = () => { const copy = () => {
setShowMenu(false) setShowMenu(false);
onCopy() onCopy();
} };
const clear = () => { const clear = () => {
setShowMenu(false) setShowMenu(false);
onClear() onClear();
} };
const remove = () => { const remove = () => {
setShowMenu(false) setShowMenu(false);
setShowRemove(false) setShowRemove(false);
onDelete() onDelete();
} };
const select = () => { const select = () => {
onSelect() onSelect();
} };
return ( return (
<Menu <Menu
visible={showMenu} visible={showMenu}
onDismiss={() => setShowMenu(false)} onDismiss={() => setShowMenu(false)}
anchor={ anchor={<IconButton onPress={() => setShowMenu(true)} icon="more-vert" />}
<IconButton
onPress={() => setShowMenu(true)}
icon='more-vert'
/>
}
> >
<Menu.Item leadingIcon='done-all' title='Select all' onPress={select} /> <Menu.Item leadingIcon="done-all" title="Select all" onPress={select} />
<Menu.Item <Menu.Item
leadingIcon='clear' leadingIcon="clear"
title='Clear' title="Clear"
onPress={clear} onPress={clear}
disabled={ids?.length === 0} disabled={ids?.length === 0}
/> />
<Menu.Item <Menu.Item
leadingIcon='edit' leadingIcon="edit"
title='Edit' title="Edit"
onPress={edit} onPress={edit}
disabled={ids?.length === 0} disabled={ids?.length === 0}
/> />
<Menu.Item <Menu.Item
leadingIcon='content-copy' leadingIcon="content-copy"
title='Copy' title="Copy"
onPress={copy} onPress={copy}
disabled={ids?.length === 0} disabled={ids?.length === 0}
/> />
<Divider /> <Divider />
<Menu.Item <Menu.Item
leadingIcon='delete' leadingIcon="delete"
onPress={() => setShowRemove(true)} onPress={() => setShowRemove(true)}
title='Delete' title="Delete"
/> />
<ConfirmDialog <ConfirmDialog
title={ids?.length === 0 ? 'Delete all' : 'Delete selected'} title={ids?.length === 0 ? "Delete all" : "Delete selected"}
show={showRemove} show={showRemove}
setShow={setShowRemove} setShow={setShowRemove}
onOk={remove} onOk={remove}
onCancel={() => setShowMenu(false)} onCancel={() => setShowMenu(false)}
> >
{ids?.length === 0 {ids?.length === 0 ? (
? <>This irreversibly deletes records from the app. Are you sure?</> <>This irreversibly deletes records from the app. Are you sure?</>
: <>This will delete {ids?.length} record(s). Are you sure?</>} ) : (
<>This will delete {ids?.length} record(s). Are you sure?</>
)}
</ConfirmDialog> </ConfirmDialog>
</Menu> </Menu>
) );
} }

View File

@ -1,7 +1,7 @@
import { StyleProp, StyleSheet, View, ViewStyle } from 'react-native' import { StyleProp, StyleSheet, View, ViewStyle } from "react-native";
import { Searchbar } from 'react-native-paper' import { Searchbar } from "react-native-paper";
import AppFab from './AppFab' import AppFab from "./AppFab";
import { PADDING } from './constants' import { PADDING } from "./constants";
export default function Page({ export default function Page({
onAdd, onAdd,
@ -10,25 +10,25 @@ export default function Page({
search, search,
style, style,
}: { }: {
children: JSX.Element | JSX.Element[] children: JSX.Element | JSX.Element[];
onAdd?: () => void onAdd?: () => void;
term: string term: string;
search: (value: string) => void search: (value: string) => void;
style?: StyleProp<ViewStyle> style?: StyleProp<ViewStyle>;
}) { }) {
return ( return (
<View style={[styles.view, style]}> <View style={[styles.view, style]}>
<Searchbar <Searchbar
placeholder='Search' placeholder="Search"
value={term} value={term}
onChangeText={search} onChangeText={search}
icon='search' icon="search"
clearIcon='clear' clearIcon="clear"
/> />
{children} {children}
{onAdd && <AppFab onPress={onAdd} />} {onAdd && <AppFab onPress={onAdd} />}
</View> </View>
) );
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
@ -36,4 +36,4 @@ const styles = StyleSheet.create({
padding: PADDING, padding: PADDING,
flexGrow: 1, flexGrow: 1,
}, },
}) });

View File

@ -2,91 +2,89 @@ import {
NavigationProp, NavigationProp,
useFocusEffect, useFocusEffect,
useNavigation, useNavigation,
} from '@react-navigation/native' } from "@react-navigation/native";
import { useCallback, useMemo, useState } from 'react' import { useCallback, useMemo, useState } from "react";
import { Text } from 'react-native' import { Text } from "react-native";
import { List } from 'react-native-paper' import { List } from "react-native-paper";
import { DARK_RIPPLE, LIGHT_RIPPLE } from './constants' import { DARK_RIPPLE, LIGHT_RIPPLE } from "./constants";
import { setRepo } from './db' import { setRepo } from "./db";
import { defaultSet } from './gym-set' import { defaultSet } from "./gym-set";
import { Plan } from './plan' import { Plan } from "./plan";
import { PlanPageParams } from './plan-page-params' import { PlanPageParams } from "./plan-page-params";
import { DAYS } from './time' import { DAYS } from "./time";
import useDark from './use-dark' import useDark from "./use-dark";
export default function PlanItem({ export default function PlanItem({
item, item,
setIds, setIds,
ids, ids,
}: { }: {
item: Plan item: Plan;
ids: number[] ids: number[];
setIds: (value: number[]) => void setIds: (value: number[]) => void;
}) { }) {
const [today, setToday] = useState<string>() const [today, setToday] = useState<string>();
const dark = useDark() const dark = useDark();
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 start = useCallback(async () => { const start = useCallback(async () => {
const workout = item.workouts.split(',')[0] const workout = item.workouts.split(",")[0];
let first = await setRepo.findOne({ let first = await setRepo.findOne({
where: { name: workout }, where: { name: workout },
order: { created: 'desc' }, order: { created: "desc" },
}) });
if (!first) first = { ...defaultSet, name: workout } if (!first) first = { ...defaultSet, name: workout };
delete first.id delete first.id;
if (ids.length === 0) { if (ids.length === 0) {
return navigation.navigate('StartPlan', { plan: item, first }) return navigation.navigate("StartPlan", { plan: item, first });
} }
const removing = ids.find((id) => id === item.id) const removing = ids.find((id) => id === item.id);
if (removing) setIds(ids.filter((id) => id !== item.id)) if (removing) setIds(ids.filter((id) => id !== item.id));
else setIds([...ids, item.id]) else setIds([...ids, item.id]);
}, [ids, setIds, item, navigation]) }, [ids, setIds, item, navigation]);
const longPress = useCallback(() => { const longPress = useCallback(() => {
if (ids.length > 0) return if (ids.length > 0) return;
setIds([item.id]) setIds([item.id]);
}, [ids.length, item.id, setIds]) }, [ids.length, item.id, setIds]);
const title = useMemo( const title = useMemo(
() => () =>
days.map((day, index) => ( days.map((day, index) => (
<Text key={day}> <Text key={day}>
{day === today {day === today ? (
? ( <Text
<Text style={{ fontWeight: "bold", textDecorationLine: "underline" }}
style={{ fontWeight: 'bold', textDecorationLine: 'underline' }} >
> {day}
{day} </Text>
</Text> ) : (
) day
: ( )}
day {index === days.length - 1 ? "" : ", "}
)}
{index === days.length - 1 ? '' : ', '}
</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 backgroundColor = useMemo(() => { const backgroundColor = useMemo(() => {
if (!ids.includes(item.id)) return if (!ids.includes(item.id)) return;
if (dark) return DARK_RIPPLE if (dark) return DARK_RIPPLE;
return LIGHT_RIPPLE return LIGHT_RIPPLE;
}, [dark, ids, item.id]) }, [dark, ids, item.id]);
return ( return (
<List.Item <List.Item
@ -96,5 +94,5 @@ export default function PlanItem({
onLongPress={longPress} onLongPress={longPress}
style={{ backgroundColor }} style={{ backgroundColor }}
/> />
) );
} }

View File

@ -2,24 +2,24 @@ import {
NavigationProp, NavigationProp,
useFocusEffect, useFocusEffect,
useNavigation, useNavigation,
} from '@react-navigation/native' } from "@react-navigation/native";
import { useCallback, useState } from 'react' import { useCallback, useState } from "react";
import { FlatList } from 'react-native' import { FlatList } from "react-native";
import { List } from 'react-native-paper' import { List } from "react-native-paper";
import { Like } from 'typeorm' import { Like } from "typeorm";
import { planRepo } from './db' import { planRepo } from "./db";
import DrawerHeader from './DrawerHeader' import DrawerHeader from "./DrawerHeader";
import ListMenu from './ListMenu' import ListMenu from "./ListMenu";
import Page from './Page' import Page from "./Page";
import { Plan } from './plan' import { Plan } from "./plan";
import { PlanPageParams } from './plan-page-params' import { PlanPageParams } from "./plan-page-params";
import PlanItem from './PlanItem' import PlanItem from "./PlanItem";
export default function PlanList() { export default function PlanList() {
const [term, setTerm] = useState('') const [term, setTerm] = useState("");
const [plans, setPlans] = useState<Plan[]>() const [plans, setPlans] = useState<Plan[]>();
const [ids, setIds] = useState<number[]>([]) const [ids, setIds] = useState<number[]>([]);
const navigation = useNavigation<NavigationProp<PlanPageParams>>() const navigation = useNavigation<NavigationProp<PlanPageParams>>();
const refresh = useCallback(async (value: string) => { const refresh = useCallback(async (value: string) => {
planRepo planRepo
@ -29,65 +29,65 @@ export default function PlanList() {
{ workouts: Like(`%${value.trim()}%`) }, { workouts: Like(`%${value.trim()}%`) },
], ],
}) })
.then(setPlans) .then(setPlans);
}, []) }, []);
useFocusEffect( useFocusEffect(
useCallback(() => { useCallback(() => {
refresh(term) refresh(term);
}, [refresh, term]), }, [refresh, term])
) );
const search = useCallback( const search = useCallback(
(value: string) => { (value: string) => {
setTerm(value) setTerm(value);
refresh(value) refresh(value);
}, },
[refresh], [refresh]
) );
const renderItem = useCallback( const renderItem = useCallback(
({ item }: { item: Plan }) => ( ({ item }: { item: Plan }) => (
<PlanItem ids={ids} setIds={setIds} item={item} key={item.id} /> <PlanItem ids={ids} setIds={setIds} item={item} key={item.id} />
), ),
[ids], [ids]
) );
const onAdd = () => const onAdd = () =>
navigation.navigate('EditPlan', { plan: { days: '', workouts: '' } }) navigation.navigate("EditPlan", { plan: { days: "", workouts: "" } });
const edit = useCallback(async () => { const edit = useCallback(async () => {
const plan = await planRepo.findOne({ where: { id: ids.pop() } }) const plan = await planRepo.findOne({ where: { id: ids.pop() } });
navigation.navigate('EditPlan', { plan }) navigation.navigate("EditPlan", { plan });
setIds([]) setIds([]);
}, [ids, navigation]) }, [ids, navigation]);
const copy = useCallback(async () => { const copy = useCallback(async () => {
const plan = await planRepo.findOne({ const plan = await planRepo.findOne({
where: { id: ids.pop() }, where: { id: ids.pop() },
}) });
delete plan.id delete plan.id;
navigation.navigate('EditPlan', { plan }) navigation.navigate("EditPlan", { plan });
setIds([]) setIds([]);
}, [ids, navigation]) }, [ids, navigation]);
const clear = useCallback(() => { const clear = useCallback(() => {
setIds([]) setIds([]);
}, []) }, []);
const remove = useCallback(async () => { const remove = useCallback(async () => {
await planRepo.delete(ids.length > 0 ? ids : {}) await planRepo.delete(ids.length > 0 ? ids : {});
await refresh(term) await refresh(term);
setIds([]) setIds([]);
}, [ids, refresh, term]) }, [ids, refresh, term]);
const select = useCallback(() => { const select = useCallback(() => {
setIds(plans.map((plan) => plan.id)) setIds(plans.map((plan) => plan.id));
}, [plans]) }, [plans]);
return ( return (
<> <>
<DrawerHeader name={ids.length > 0 ? `${ids.length} selected` : 'Plans'}> <DrawerHeader name={ids.length > 0 ? `${ids.length} selected` : "Plans"}>
<ListMenu <ListMenu
onClear={clear} onClear={clear}
onCopy={copy} onCopy={copy}
@ -98,22 +98,20 @@ export default function PlanList() {
/> />
</DrawerHeader> </DrawerHeader>
<Page onAdd={onAdd} term={term} search={search}> <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' description="A plan is a list of workouts for certain days."
description='A plan is a list of workouts for certain days.' />
/> ) : (
) <FlatList
: ( style={{ flex: 1 }}
<FlatList data={plans}
style={{ flex: 1 }} renderItem={renderItem}
data={plans} keyExtractor={(set) => set.id?.toString() || ""}
renderItem={renderItem} />
keyExtractor={(set) => set.id?.toString() || ''} )}
/>
)}
</Page> </Page>
</> </>
) );
} }

View File

@ -1,21 +1,21 @@
import { createStackNavigator } from '@react-navigation/stack' import { createStackNavigator } from "@react-navigation/stack";
import EditPlan from './EditPlan' import EditPlan from "./EditPlan";
import EditSet from './EditSet' 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 (
<Stack.Navigator <Stack.Navigator
screenOptions={{ headerShown: false, animationEnabled: false }} screenOptions={{ headerShown: false, animationEnabled: false }}
> >
<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.Screen name="EditSet" component={EditSet} />
</Stack.Navigator> </Stack.Navigator>
) );
} }

View File

@ -1,57 +1,57 @@
import { createDrawerNavigator } from '@react-navigation/drawer' import { createDrawerNavigator } from "@react-navigation/drawer";
import { IconButton } from 'react-native-paper' import { IconButton } from "react-native-paper";
import GraphsPage from './GraphsPage' import GraphsPage from "./GraphsPage";
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 SettingsPage from './SettingsPage' import SettingsPage from "./SettingsPage";
import TimerPage from './TimerPage' import TimerPage from "./TimerPage";
import useDark from './use-dark' import useDark from "./use-dark";
import WorkoutsPage from './WorkoutsPage' import WorkoutsPage from "./WorkoutsPage";
const Drawer = createDrawerNavigator<DrawerParamList>() const Drawer = createDrawerNavigator<DrawerParamList>();
export default function Routes() { export default function Routes() {
const dark = useDark() const dark = useDark();
return ( return (
<Drawer.Navigator <Drawer.Navigator
screenOptions={{ screenOptions={{
headerTintColor: dark ? 'white' : 'black', headerTintColor: dark ? "white" : "black",
swipeEdgeWidth: 1000, swipeEdgeWidth: 1000,
headerShown: false, headerShown: false,
}} }}
> >
<Drawer.Screen <Drawer.Screen
name='Home' name="Home"
component={HomePage} component={HomePage}
options={{ drawerIcon: () => <IconButton icon='home' /> }} options={{ drawerIcon: () => <IconButton icon="home" /> }}
/> />
<Drawer.Screen <Drawer.Screen
name='Plans' name="Plans"
component={PlanPage} component={PlanPage}
options={{ drawerIcon: () => <IconButton icon='event' /> }} options={{ drawerIcon: () => <IconButton icon="event" /> }}
/> />
<Drawer.Screen <Drawer.Screen
name='Graphs' name="Graphs"
component={GraphsPage} component={GraphsPage}
options={{ drawerIcon: () => <IconButton icon='insights' /> }} options={{ drawerIcon: () => <IconButton icon="insights" /> }}
/> />
<Drawer.Screen <Drawer.Screen
name='Workouts' name="Workouts"
component={WorkoutsPage} component={WorkoutsPage}
options={{ drawerIcon: () => <IconButton icon='fitness-center' /> }} options={{ drawerIcon: () => <IconButton icon="fitness-center" /> }}
/> />
<Drawer.Screen <Drawer.Screen
name='Timer' name="Timer"
component={TimerPage} component={TimerPage}
options={{ drawerIcon: () => <IconButton icon='access-time' /> }} options={{ drawerIcon: () => <IconButton icon="access-time" /> }}
/> />
<Drawer.Screen <Drawer.Screen
name='Settings' name="Settings"
component={SettingsPage} component={SettingsPage}
options={{ drawerIcon: () => <IconButton icon='settings' /> }} options={{ drawerIcon: () => <IconButton icon="settings" /> }}
/> />
</Drawer.Navigator> </Drawer.Navigator>
) );
} }

View File

@ -1,12 +1,12 @@
import React, { useCallback, useMemo, useState } from 'react' import React, { useCallback, useMemo, useState } from "react";
import { View } from 'react-native' import { View } from "react-native";
import { Button, Menu, Subheading } from 'react-native-paper' import { Button, Menu, Subheading } from "react-native-paper";
import { ITEM_PADDING } from './constants' import { ITEM_PADDING } from "./constants";
export interface Item { export interface Item {
value: string value: string;
label: string label: string;
color?: string color?: string;
} }
function Select({ function Select({
@ -15,31 +15,31 @@ function Select({
items, items,
label, label,
}: { }: {
value: string value: string;
onChange: (value: string) => void onChange: (value: string) => void;
items: Item[] items: Item[];
label?: string label?: string;
}) { }) {
const [show, setShow] = useState(false) const [show, setShow] = useState(false);
const selected = useMemo( const selected = useMemo(
() => items.find((item) => item.value === value) || items[0], () => items.find((item) => item.value === value) || items[0],
[items, value], [items, value]
) );
const handlePress = useCallback( const handlePress = useCallback(
(newValue: string) => { (newValue: string) => {
onChange(newValue) onChange(newValue);
setShow(false) setShow(false);
}, },
[onChange], [onChange]
) );
return ( return (
<View <View
style={{ style={{
flexDirection: 'row', flexDirection: "row",
alignItems: 'center', alignItems: "center",
paddingLeft: ITEM_PADDING, paddingLeft: ITEM_PADDING,
}} }}
> >
@ -51,7 +51,7 @@ function Select({
<Button <Button
onPress={() => setShow(true)} onPress={() => setShow(true)}
style={{ style={{
alignSelf: 'flex-start', alignSelf: "flex-start",
}} }}
> >
{selected?.label} {selected?.label}
@ -68,7 +68,7 @@ function Select({
))} ))}
</Menu> </Menu>
</View> </View>
) );
} }
export default React.memo(Select) export default React.memo(Select);

View File

@ -1,13 +1,13 @@
import { NavigationProp, useNavigation } from '@react-navigation/native' import { NavigationProp, useNavigation } from "@react-navigation/native";
import { format } from 'date-fns' import { format } from "date-fns";
import { useCallback, useMemo } from 'react' import { useCallback, useMemo } from "react";
import { Image } from 'react-native' import { Image } from "react-native";
import { List, Text } from 'react-native-paper' import { List, Text } from "react-native-paper";
import { DARK_RIPPLE, LIGHT_RIPPLE } from './constants' import { DARK_RIPPLE, LIGHT_RIPPLE } from "./constants";
import GymSet from './gym-set' import GymSet from "./gym-set";
import { HomePageParams } from './home-page-params' import { HomePageParams } from "./home-page-params";
import Settings from './settings' import Settings from "./settings";
import useDark from './use-dark' import useDark from "./use-dark";
export default function SetItem({ export default function SetItem({
item, item,
@ -15,66 +15,63 @@ export default function SetItem({
ids, ids,
setIds, setIds,
}: { }: {
item: GymSet item: GymSet;
onRemove: () => void onRemove: () => void;
settings: Settings settings: Settings;
ids: number[] ids: number[];
setIds: (value: number[]) => void setIds: (value: number[]) => void;
}) { }) {
const dark = useDark() const dark = useDark();
const navigation = useNavigation<NavigationProp<HomePageParams>>() const navigation = useNavigation<NavigationProp<HomePageParams>>();
const longPress = useCallback(() => { const longPress = useCallback(() => {
if (ids.length > 0) return if (ids.length > 0) return;
setIds([item.id]) setIds([item.id]);
}, [ids.length, item.id, setIds]) }, [ids.length, item.id, setIds]);
const press = useCallback(() => { const press = useCallback(() => {
if (ids.length === 0) return navigation.navigate('EditSet', { set: item }) if (ids.length === 0) return navigation.navigate("EditSet", { set: item });
const removing = ids.find((id) => id === item.id) const removing = ids.find((id) => id === item.id);
if (removing) setIds(ids.filter((id) => id !== item.id)) if (removing) setIds(ids.filter((id) => id !== item.id));
else setIds([...ids, item.id]) else setIds([...ids, item.id]);
}, [ids, item, navigation, setIds]) }, [ids, item, navigation, setIds]);
const backgroundColor = useMemo(() => { const backgroundColor = useMemo(() => {
if (!ids.includes(item.id)) return if (!ids.includes(item.id)) return;
if (dark) return DARK_RIPPLE if (dark) return DARK_RIPPLE;
return LIGHT_RIPPLE return LIGHT_RIPPLE;
}, [dark, ids, item.id]) }, [dark, ids, item.id]);
const left = useCallback(() => { const left = useCallback(() => {
if (!settings.images || !item.image) return null if (!settings.images || !item.image) return null;
return ( return (
<Image <Image source={{ uri: item.image }} style={{ height: 75, width: 75 }} />
source={{ uri: item.image }} );
style={{ height: 75, width: 75 }} }, [item.image, settings.images]);
/>
)
}, [item.image, settings.images])
const right = useCallback(() => { const right = useCallback(() => {
if (!settings.showDate) return null if (!settings.showDate) return null;
return ( return (
<Text <Text
style={{ style={{
alignSelf: 'center', alignSelf: "center",
color: dark ? '#909090ff' : '#717171ff', color: dark ? "#909090ff" : "#717171ff",
}} }}
> >
{format(new Date(item.created), settings.date || 'P')} {format(new Date(item.created), settings.date || "P")}
</Text> </Text>
) );
}, [settings.showDate, item.created, settings.date, dark]) }, [settings.showDate, item.created, settings.date, dark]);
return ( return (
<List.Item <List.Item
onPress={press} onPress={press}
title={item.name} title={item.name}
description={`${item.reps} x ${item.weight}${item.unit || 'kg'}`} description={`${item.reps} x ${item.weight}${item.unit || "kg"}`}
onLongPress={longPress} onLongPress={longPress}
style={{ backgroundColor }} style={{ backgroundColor }}
left={left} left={left}
right={right} right={right}
/> />
) );
} }

View File

@ -2,50 +2,50 @@ import {
NavigationProp, NavigationProp,
useFocusEffect, useFocusEffect,
useNavigation, useNavigation,
} from '@react-navigation/native' } from "@react-navigation/native";
import { useCallback, useState } from 'react' import { useCallback, useState } from "react";
import { FlatList } from 'react-native' import { FlatList } from "react-native";
import { List } from 'react-native-paper' import { List } from "react-native-paper";
import { Like } from 'typeorm' import { Like } from "typeorm";
import { getNow, setRepo, settingsRepo } from './db' import { getNow, setRepo, settingsRepo } from "./db";
import DrawerHeader from './DrawerHeader' import DrawerHeader from "./DrawerHeader";
import GymSet, { defaultSet } from './gym-set' import GymSet, { defaultSet } from "./gym-set";
import { HomePageParams } from './home-page-params' import { HomePageParams } from "./home-page-params";
import ListMenu from './ListMenu' import ListMenu from "./ListMenu";
import Page from './Page' import Page from "./Page";
import SetItem from './SetItem' import SetItem from "./SetItem";
import Settings from './settings' import Settings from "./settings";
const limit = 15 const limit = 15;
export default function SetList() { export default function SetList() {
const [sets, setSets] = useState<GymSet[]>([]) const [sets, setSets] = useState<GymSet[]>([]);
const [offset, setOffset] = useState(0) const [offset, setOffset] = useState(0);
const [term, setTerm] = useState('') const [term, setTerm] = useState("");
const [end, setEnd] = useState(false) const [end, setEnd] = useState(false);
const [settings, setSettings] = useState<Settings>() const [settings, setSettings] = useState<Settings>();
const [ids, setIds] = useState<number[]>([]) const [ids, setIds] = useState<number[]>([]);
const navigation = useNavigation<NavigationProp<HomePageParams>>() const navigation = useNavigation<NavigationProp<HomePageParams>>();
const refresh = useCallback(async (value: string) => { const refresh = useCallback(async (value: string) => {
const newSets = await setRepo.find({ const newSets = await setRepo.find({
where: { name: Like(`%${value.trim()}%`), hidden: 0 as any }, where: { name: Like(`%${value.trim()}%`), hidden: 0 as any },
take: limit, take: limit,
skip: 0, skip: 0,
order: { created: 'DESC' }, order: { created: "DESC" },
}) });
console.log(`${SetList.name}.refresh:`, { value, limit }) console.log(`${SetList.name}.refresh:`, { value, limit });
setSets(newSets) setSets(newSets);
setOffset(0) setOffset(0);
setEnd(false) setEnd(false);
}, []) }, []);
useFocusEffect( useFocusEffect(
useCallback(() => { useCallback(() => {
refresh(term) refresh(term);
settingsRepo.findOne({ where: {} }).then(setSettings) settingsRepo.findOne({ where: {} }).then(setSettings);
}, [refresh, term]), }, [refresh, term])
) );
const renderItem = useCallback( const renderItem = useCallback(
({ item }: { item: GymSet }) => ( ({ item }: { item: GymSet }) => (
@ -58,75 +58,75 @@ export default function SetList() {
setIds={setIds} setIds={setIds}
/> />
), ),
[refresh, term, settings, ids], [refresh, term, settings, ids]
) );
const next = useCallback(async () => { const next = useCallback(async () => {
if (end) return if (end) return;
const newOffset = offset + limit const newOffset = offset + limit;
console.log(`${SetList.name}.next:`, { offset, newOffset, term }) console.log(`${SetList.name}.next:`, { offset, newOffset, term });
const newSets = await setRepo.find({ const newSets = await setRepo.find({
where: { name: Like(`%${term}%`), hidden: 0 as any }, where: { name: Like(`%${term}%`), hidden: 0 as any },
take: limit, take: limit,
skip: newOffset, skip: newOffset,
order: { created: 'DESC' }, order: { created: "DESC" },
}) });
if (newSets.length === 0) return setEnd(true) if (newSets.length === 0) return setEnd(true);
if (!sets) return if (!sets) return;
setSets([...sets, ...newSets]) setSets([...sets, ...newSets]);
if (newSets.length < limit) return setEnd(true) if (newSets.length < limit) return setEnd(true);
setOffset(newOffset) setOffset(newOffset);
}, [term, end, offset, sets]) }, [term, end, offset, sets]);
const onAdd = useCallback(async () => { const onAdd = useCallback(async () => {
const now = await getNow() const now = await getNow();
let set = sets[0] let set = sets[0];
if (!set) set = { ...defaultSet } if (!set) set = { ...defaultSet };
set.created = now set.created = now;
delete set.id delete set.id;
navigation.navigate('EditSet', { set }) navigation.navigate("EditSet", { set });
}, [navigation, sets]) }, [navigation, sets]);
const search = useCallback( const search = useCallback(
(value: string) => { (value: string) => {
setTerm(value) setTerm(value);
refresh(value) refresh(value);
}, },
[refresh], [refresh]
) );
const edit = useCallback(() => { const edit = useCallback(() => {
navigation.navigate('EditSets', { ids }) navigation.navigate("EditSets", { ids });
setIds([]) setIds([]);
}, [ids, navigation]) }, [ids, navigation]);
const copy = useCallback(async () => { const copy = useCallback(async () => {
const set = await setRepo.findOne({ const set = await setRepo.findOne({
where: { id: ids.pop() }, where: { id: ids.pop() },
}) });
delete set.id delete set.id;
delete set.created delete set.created;
navigation.navigate('EditSet', { set }) navigation.navigate("EditSet", { set });
setIds([]) setIds([]);
}, [ids, navigation]) }, [ids, navigation]);
const clear = useCallback(() => { const clear = useCallback(() => {
setIds([]) setIds([]);
}, []) }, []);
const remove = useCallback(async () => { const remove = useCallback(async () => {
setIds([]) setIds([]);
await setRepo.delete(ids.length > 0 ? ids : {}) await setRepo.delete(ids.length > 0 ? ids : {});
await refresh(term) await refresh(term);
}, [ids, refresh, term]) }, [ids, refresh, term]);
const select = useCallback(() => { const select = useCallback(() => {
setIds(sets.map((set) => set.id)) setIds(sets.map((set) => set.id));
}, [sets]) }, [sets]);
return ( return (
<> <>
<DrawerHeader name={ids.length > 0 ? `${ids.length} selected` : 'Home'}> <DrawerHeader name={ids.length > 0 ? `${ids.length} selected` : "Home"}>
<ListMenu <ListMenu
onClear={clear} onClear={clear}
onCopy={copy} onCopy={copy}
@ -138,24 +138,22 @@ export default function SetList() {
</DrawerHeader> </DrawerHeader>
<Page onAdd={onAdd} term={term} search={search}> <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
data={sets}
style={{ flex: 1 }}
renderItem={renderItem}
onEndReached={next}
/> />
) )
: ( )}
settings && (
<FlatList
data={sets}
style={{ flex: 1 }}
renderItem={renderItem}
onEndReached={next}
/>
)
)}
</Page> </Page>
</> </>
) );
} }

View File

@ -1,34 +1,33 @@
import { View } from 'react-native' import { View } from "react-native";
import { Button, Subheading } from 'react-native-paper' import { Button, Subheading } from "react-native-paper";
import { ITEM_PADDING } from './constants' import { ITEM_PADDING } from "./constants";
export default function SettingButton( export default function SettingButton({
{ name: text, label, onPress }: { name: text,
name: string label,
label?: string onPress,
onPress: () => void }: {
}, name: string;
) { label?: string;
onPress: () => void;
}) {
if (label) { if (label) {
return ( return (
<View <View
style={{ style={{
flexDirection: 'row', flexDirection: "row",
alignItems: 'center', alignItems: "center",
paddingLeft: ITEM_PADDING, paddingLeft: ITEM_PADDING,
}} }}
> >
<Subheading style={{ width: 100 }}>{label}</Subheading> <Subheading style={{ width: 100 }}>{label}</Subheading>
<Button onPress={onPress}>{text}</Button> <Button onPress={onPress}>{text}</Button>
</View> </View>
) );
} }
return ( return (
<Button <Button style={{ alignSelf: "flex-start" }} onPress={onPress}>
style={{ alignSelf: 'flex-start' }}
onPress={onPress}
>
{text} {text}
</Button> </Button>
) );
} }

View File

@ -1,57 +1,57 @@
import { NavigationProp, useNavigation } from '@react-navigation/native' import { NavigationProp, useNavigation } from "@react-navigation/native";
import { format } from 'date-fns' import { format } from "date-fns";
import { useCallback, useEffect, useMemo, useState } from 'react' import { useCallback, useEffect, useMemo, useState } from "react";
import { useForm } from 'react-hook-form' import { useForm } from "react-hook-form";
import { NativeModules, ScrollView } from 'react-native' import { NativeModules, ScrollView } from "react-native";
import DocumentPicker from 'react-native-document-picker' import DocumentPicker from "react-native-document-picker";
import { Dirs, FileSystem } from 'react-native-file-access' import { Dirs, FileSystem } from "react-native-file-access";
import ConfirmDialog from './ConfirmDialog' import ConfirmDialog from "./ConfirmDialog";
import { MARGIN } from './constants' import { MARGIN } from "./constants";
import { AppDataSource } from './data-source' import { AppDataSource } from "./data-source";
import { setRepo, settingsRepo } from './db' import { setRepo, settingsRepo } from "./db";
import { DrawerParamList } from './drawer-param-list' import { DrawerParamList } from "./drawer-param-list";
import DrawerHeader from './DrawerHeader' import DrawerHeader from "./DrawerHeader";
import Input from './input' import Input from "./input";
import { darkOptions, lightOptions, themeOptions } from './options' import { darkOptions, lightOptions, themeOptions } from "./options";
import Page from './Page' import Page from "./Page";
import Select from './Select' import Select from "./Select";
import SettingButton from './SettingButton' import SettingButton from "./SettingButton";
import Settings from './settings' import Settings from "./settings";
import Switch from './Switch' import Switch from "./Switch";
import { toast } from './toast' import { toast } from "./toast";
import { useTheme } from './use-theme' import { useTheme } from "./use-theme";
const twelveHours = [ const twelveHours = [
'dd/LL/yyyy', "dd/LL/yyyy",
'dd/LL/yyyy, p', "dd/LL/yyyy, p",
'ccc p', "ccc p",
'p', "p",
'yyyy-MM-d', "yyyy-MM-d",
'yyyy-MM-d, p', "yyyy-MM-d, p",
'yyyy.MM.d', "yyyy.MM.d",
] ];
const twentyFours = [ const twentyFours = [
'dd/LL/yyyy', "dd/LL/yyyy",
'dd/LL/yyyy, k:m', "dd/LL/yyyy, k:m",
'ccc k:m', "ccc k:m",
'k:m', "k:m",
'yyyy-MM-d', "yyyy-MM-d",
'yyyy-MM-d, k:m', "yyyy-MM-d, k:m",
'yyyy.MM.d', "yyyy.MM.d",
] ];
export default function SettingsPage() { export default function SettingsPage() {
const [ignoring, setIgnoring] = useState(false) const [ignoring, setIgnoring] = useState(false);
const [term, setTerm] = useState('') const [term, setTerm] = useState("");
const [formatOptions, setFormatOptions] = useState<string[]>(twelveHours) const [formatOptions, setFormatOptions] = useState<string[]>(twelveHours);
const [importing, setImporting] = useState(false) const [importing, setImporting] = useState(false);
const [deleting, setDeleting] = useState(false) const [deleting, setDeleting] = useState(false);
const { reset } = useNavigation<NavigationProp<DrawerParamList>>() const { reset } = useNavigation<NavigationProp<DrawerParamList>>();
const { watch, setValue } = useForm<Settings>({ const { watch, setValue } = useForm<Settings>({
defaultValues: () => settingsRepo.findOne({ where: {} }), defaultValues: () => settingsRepo.findOne({ where: {} }),
}) });
const settings = watch() const settings = watch();
const { const {
theme, theme,
@ -60,16 +60,16 @@ export default function SettingsPage() {
setLightColor, setLightColor,
darkColor, darkColor,
setDarkColor, setDarkColor,
} = useTheme() } = useTheme();
useEffect(() => { useEffect(() => {
NativeModules.SettingsModule.ignoringBattery(setIgnoring) NativeModules.SettingsModule.ignoringBattery(setIgnoring);
NativeModules.SettingsModule.is24().then((is24: boolean) => { NativeModules.SettingsModule.is24().then((is24: boolean) => {
console.log(`${SettingsPage.name}.focus:`, { is24 }) console.log(`${SettingsPage.name}.focus:`, { is24 });
if (is24) setFormatOptions(twentyFours) if (is24) setFormatOptions(twentyFours);
else setFormatOptions(twelveHours) else setFormatOptions(twelveHours);
}) });
}, []) }, []);
const update = useCallback((key: keyof Settings, value: unknown) => { const update = useCallback((key: keyof Settings, value: unknown) => {
return settingsRepo return settingsRepo
@ -77,98 +77,98 @@ export default function SettingsPage() {
.update() .update()
.set({ [key]: value }) .set({ [key]: value })
.printSql() .printSql()
.execute() .execute();
}, []) }, []);
const soundString = useMemo(() => { const soundString = useMemo(() => {
if (!settings.sound) return null if (!settings.sound) return null;
const split = settings.sound.split('/') const split = settings.sound.split("/");
return split.pop() return split.pop();
}, [settings.sound]) }, [settings.sound]);
const changeSound = useCallback(async () => { const changeSound = useCallback(async () => {
const { fileCopyUri } = await DocumentPicker.pickSingle({ const { fileCopyUri } = await DocumentPicker.pickSingle({
type: DocumentPicker.types.audio, type: DocumentPicker.types.audio,
copyTo: 'documentDirectory', copyTo: "documentDirectory",
}) });
if (!fileCopyUri) return if (!fileCopyUri) return;
setValue('sound', fileCopyUri) setValue("sound", fileCopyUri);
await update('sound', fileCopyUri) await update("sound", fileCopyUri);
toast('Sound will play after rest timers.') toast("Sound will play after rest timers.");
}, [setValue, update]) }, [setValue, update]);
const switches: Input<boolean>[] = useMemo( const switches: Input<boolean>[] = useMemo(
() => [ () => [
{ name: 'Rest timers', value: settings.alarm, key: 'alarm' }, { name: "Rest timers", value: settings.alarm, key: "alarm" },
{ name: 'Vibrate', value: settings.vibrate, key: 'vibrate' }, { name: "Vibrate", value: settings.vibrate, key: "vibrate" },
{ name: 'Disable sound', value: settings.noSound, key: 'noSound' }, { name: "Disable sound", value: settings.noSound, key: "noSound" },
{ name: 'Notifications', value: settings.notify, key: 'notify' }, { name: "Notifications", value: settings.notify, key: "notify" },
{ name: 'Show images', value: settings.images, key: 'images' }, { name: "Show images", value: settings.images, key: "images" },
{ name: 'Show unit', value: settings.showUnit, key: 'showUnit' }, { name: "Show unit", value: settings.showUnit, key: "showUnit" },
{ name: 'Show steps', value: settings.steps, key: 'steps' }, { name: "Show steps", value: settings.steps, key: "steps" },
{ name: 'Show date', value: settings.showDate, key: 'showDate' }, { name: "Show date", value: settings.showDate, key: "showDate" },
{ name: 'Automatic backup', value: settings.backup, key: 'backup' }, { name: "Automatic backup", value: settings.backup, key: "backup" },
], ],
[settings], [settings]
) );
const filter = useCallback( const filter = useCallback(
({ name }) => name.toLowerCase().includes(term.toLowerCase()), ({ name }) => name.toLowerCase().includes(term.toLowerCase()),
[term], [term]
) );
const changeBoolean = useCallback( const changeBoolean = useCallback(
async (key: keyof Settings, value: boolean) => { async (key: keyof Settings, value: boolean) => {
setValue(key, value) setValue(key, value);
await update(key, value) await update(key, value);
switch (key) { switch (key) {
case 'alarm': case "alarm":
if (value) toast('Timers will now run after each set.') if (value) toast("Timers will now run after each set.");
else toast('Stopped timers running after each set.') else toast("Stopped timers running after each set.");
if (value && !ignoring) NativeModules.SettingsModule.ignoreBattery() if (value && !ignoring) NativeModules.SettingsModule.ignoreBattery();
return return;
case 'vibrate': case "vibrate":
if (value) toast('Alarms will now vibrate.') if (value) toast("Alarms will now vibrate.");
else toast('Alarms will no longer vibrate.') else toast("Alarms will no longer vibrate.");
return return;
case 'notify': case "notify":
if (value) toast('Show notifications for new records.') if (value) toast("Show notifications for new records.");
else toast('Stopped notifications for new records.') else toast("Stopped notifications for new records.");
return return;
case 'images': case "images":
if (value) toast('Show images for sets.') if (value) toast("Show images for sets.");
else toast('Hid images for sets.') else toast("Hid images for sets.");
return return;
case 'showUnit': case "showUnit":
if (value) toast('Show option to select unit for sets.') if (value) toast("Show option to select unit for sets.");
else toast('Hid unit option for sets.') else toast("Hid unit option for sets.");
return return;
case 'steps': case "steps":
if (value) toast('Show steps for a workout.') if (value) toast("Show steps for a workout.");
else toast('Hid steps for workouts.') else toast("Hid steps for workouts.");
return return;
case 'showDate': case "showDate":
if (value) toast('Show date for sets.') if (value) toast("Show date for sets.");
else toast('Hid date on sets.') else toast("Hid date on sets.");
return return;
case 'noSound': case "noSound":
if (value) toast('Disable sound on rest timer alarms.') if (value) toast("Disable sound on rest timer alarms.");
else toast('Enabled sound for rest timer alarms.') else toast("Enabled sound for rest timer alarms.");
return return;
case 'backup': case "backup":
if (value) { if (value) {
const result = await DocumentPicker.pickDirectory() const result = await DocumentPicker.pickDirectory();
toast('Backup database daily.') toast("Backup database daily.");
NativeModules.BackupModule.start(result.uri) NativeModules.BackupModule.start(result.uri);
} else { } else {
toast('Stopped backing up daily') toast("Stopped backing up daily");
NativeModules.BackupModule.stop() NativeModules.BackupModule.stop();
} }
return return;
} }
}, },
[ignoring, setValue, update], [ignoring, setValue, update]
) );
const renderSwitch = useCallback( const renderSwitch = useCallback(
(item: Input<boolean>) => ( (item: Input<boolean>) => (
@ -179,69 +179,69 @@ export default function SettingsPage() {
title={item.name} title={item.name}
/> />
), ),
[changeBoolean], [changeBoolean]
) );
const switchesMarkup = useMemo( const switchesMarkup = useMemo(
() => switches.filter(filter).map((s) => renderSwitch(s)), () => switches.filter(filter).map((s) => renderSwitch(s)),
[filter, switches, renderSwitch], [filter, switches, renderSwitch]
) );
const changeString = useCallback( const changeString = useCallback(
async (key: keyof Settings, value: string) => { async (key: keyof Settings, value: string) => {
setValue(key, value) setValue(key, value);
await update(key, value) await update(key, value);
switch (key) { switch (key) {
case 'date': case "date":
return toast('Changed date format') return toast("Changed date format");
case 'darkColor': case "darkColor":
setDarkColor(value) setDarkColor(value);
return toast('Set primary color for dark mode.') return toast("Set primary color for dark mode.");
case 'lightColor': case "lightColor":
setLightColor(value) setLightColor(value);
return toast('Set primary color for light mode.') return toast("Set primary color for light mode.");
case 'vibrate': case "vibrate":
return toast('Set primary color for light mode.') return toast("Set primary color for light mode.");
case 'sound': case "sound":
return toast('Sound will play after rest timers.') return toast("Sound will play after rest timers.");
case 'theme': case "theme":
setTheme(value as string) setTheme(value as string);
if (value === 'dark') toast('Theme will always be dark.') if (value === "dark") toast("Theme will always be dark.");
else if (value === 'light') toast('Theme will always be light.') else if (value === "light") toast("Theme will always be light.");
else if (value === 'system') toast('Theme will follow system.') else if (value === "system") toast("Theme will follow system.");
return return;
} }
}, },
[update, setTheme, setDarkColor, setLightColor, setValue], [update, setTheme, setDarkColor, setLightColor, setValue]
) );
const selects: Input<string>[] = useMemo(() => { const selects: Input<string>[] = useMemo(() => {
const today = new Date() const today = new Date();
return [ return [
{ name: 'Theme', value: theme, items: themeOptions, key: 'theme' }, { name: "Theme", value: theme, items: themeOptions, key: "theme" },
{ {
name: 'Dark color', name: "Dark color",
value: darkColor, value: darkColor,
items: lightOptions, items: lightOptions,
key: 'darkColor', key: "darkColor",
}, },
{ {
name: 'Light color', name: "Light color",
value: lightColor, value: lightColor,
items: darkOptions, items: darkOptions,
key: 'lightColor', key: "lightColor",
}, },
{ {
name: 'Date format', name: "Date format",
value: settings.date, value: settings.date,
items: formatOptions.map((option) => ({ items: formatOptions.map((option) => ({
label: format(today, option), label: format(today, option),
value: option, value: option,
})), })),
key: 'date', key: "date",
}, },
] ];
}, [settings, darkColor, formatOptions, theme, lightColor]) }, [settings, darkColor, formatOptions, theme, lightColor]);
const renderSelect = useCallback( const renderSelect = useCallback(
(item: Input<string>) => ( (item: Input<string>) => (
@ -253,74 +253,72 @@ export default function SettingsPage() {
items={item.items} items={item.items}
/> />
), ),
[changeString], [changeString]
) );
const selectsMarkup = useMemo( const selectsMarkup = useMemo(
() => selects.filter(filter).map(renderSelect), () => selects.filter(filter).map(renderSelect),
[filter, selects, renderSelect], [filter, selects, renderSelect]
) );
const confirmDelete = useCallback(async () => { const confirmDelete = useCallback(async () => {
setDeleting(false) setDeleting(false);
await AppDataSource.dropDatabase() await AppDataSource.dropDatabase();
await AppDataSource.destroy() await AppDataSource.destroy();
await AppDataSource.initialize() await AppDataSource.initialize();
toast('Database deleted.') toast("Database deleted.");
}, []) }, []);
const confirmImport = useCallback(async () => { const confirmImport = useCallback(async () => {
setImporting(false) setImporting(false);
await AppDataSource.destroy() await AppDataSource.destroy();
const file = await DocumentPicker.pickSingle() const file = await DocumentPicker.pickSingle();
await FileSystem.cp(file.uri, Dirs.DatabaseDir + '/massive.db') await FileSystem.cp(file.uri, Dirs.DatabaseDir + "/massive.db");
await AppDataSource.initialize() await AppDataSource.initialize();
await setRepo.createQueryBuilder().update().set({ image: null }).execute() await setRepo.createQueryBuilder().update().set({ image: null }).execute();
await update('sound', null) await update("sound", null);
const { alarm, backup } = await settingsRepo.findOne({ where: {} }) const { alarm, backup } = await settingsRepo.findOne({ where: {} });
console.log({ backup }) console.log({ backup });
const directory = await DocumentPicker.pickDirectory() const directory = await DocumentPicker.pickDirectory();
if (backup) NativeModules.BackupModule.start(directory.uri) if (backup) NativeModules.BackupModule.start(directory.uri);
else NativeModules.BackupModule.stop() else NativeModules.BackupModule.stop();
NativeModules.SettingsModule.ignoringBattery( NativeModules.SettingsModule.ignoringBattery((isIgnoring: boolean) => {
(isIgnoring: boolean) => { if (alarm && !isIgnoring) NativeModules.SettingsModule.ignoreBattery();
if (alarm && !isIgnoring) NativeModules.SettingsModule.ignoreBattery() reset({ index: 0, routes: [{ name: "Settings" }] });
reset({ index: 0, routes: [{ name: 'Settings' }] }) });
}, }, [reset, update]);
)
}, [reset, update])
const exportDatabase = useCallback(async () => { const exportDatabase = useCallback(async () => {
const path = Dirs.DatabaseDir + '/massive.db' const path = Dirs.DatabaseDir + "/massive.db";
await FileSystem.cpExternal(path, 'massive.db', 'downloads') await FileSystem.cpExternal(path, "massive.db", "downloads");
toast('Database exported. Check downloads.') toast("Database exported. Check downloads.");
}, []) }, []);
const buttons = useMemo( const buttons = useMemo(
() => [ () => [
{ {
name: soundString || 'Default', name: soundString || "Default",
onPress: changeSound, onPress: changeSound,
label: 'Alarm sound', label: "Alarm sound",
}, },
{ name: 'Export database', onPress: exportDatabase }, { name: "Export database", onPress: exportDatabase },
{ name: 'Import database', onPress: () => setImporting(true) }, { name: "Import database", onPress: () => setImporting(true) },
{ name: 'Delete database', onPress: () => setDeleting(true) }, { name: "Delete database", onPress: () => setDeleting(true) },
], ],
[changeSound, exportDatabase, soundString], [changeSound, exportDatabase, soundString]
) );
const buttonsMarkup = useMemo( const buttonsMarkup = useMemo(
() => () =>
buttons.filter(filter).map((button) => ( buttons
<SettingButton {...button} key={button.name} /> .filter(filter)
)), .map((button) => <SettingButton {...button} key={button.name} />),
[buttons, filter], [buttons, filter]
) );
return ( return (
<> <>
<DrawerHeader name='Settings' /> <DrawerHeader name="Settings" />
<Page term={term} search={setTerm} style={{ flexGrow: 1 }}> <Page term={term} search={setTerm} style={{ flexGrow: 1 }}>
<ScrollView style={{ marginTop: MARGIN, flex: 1 }}> <ScrollView style={{ marginTop: MARGIN, flex: 1 }}>
@ -331,7 +329,7 @@ export default function SettingsPage() {
</Page> </Page>
<ConfirmDialog <ConfirmDialog
title='Are you sure?' title="Are you sure?"
onOk={confirmImport} onOk={confirmImport}
setShow={setImporting} setShow={setImporting}
show={importing} show={importing}
@ -341,7 +339,7 @@ export default function SettingsPage() {
</ConfirmDialog> </ConfirmDialog>
<ConfirmDialog <ConfirmDialog
title='Are you sure?' title="Are you sure?"
onOk={confirmDelete} onOk={confirmDelete}
setShow={setDeleting} setShow={setDeleting}
show={deleting} show={deleting}
@ -350,5 +348,5 @@ export default function SettingsPage() {
reversed! reversed!
</ConfirmDialog> </ConfirmDialog>
</> </>
) );
} }

View File

@ -1,23 +1,20 @@
import { useNavigation } from '@react-navigation/native' import { useNavigation } from "@react-navigation/native";
import { Appbar, IconButton } from 'react-native-paper' import { Appbar, IconButton } from "react-native-paper";
export default function StackHeader({ export default function StackHeader({
title, title,
children, children,
}: { }: {
title: string title: string;
children?: JSX.Element | JSX.Element[] children?: JSX.Element | JSX.Element[];
}) { }) {
const navigation = useNavigation() const navigation = useNavigation();
return ( return (
<Appbar.Header> <Appbar.Header>
<IconButton <IconButton icon="arrow-back" onPress={navigation.goBack} />
icon='arrow-back'
onPress={navigation.goBack}
/>
<Appbar.Content title={title} /> <Appbar.Content title={title} />
{children} {children}
</Appbar.Header> </Appbar.Header>
) );
} }

View File

@ -4,46 +4,46 @@ import {
useFocusEffect, useFocusEffect,
useNavigation, useNavigation,
useRoute, useRoute,
} from '@react-navigation/native' } from "@react-navigation/native";
import { useCallback, useMemo, useRef, useState } from 'react' import { useCallback, useMemo, useRef, useState } from "react";
import { FlatList, NativeModules, TextInput, View } from 'react-native' import { FlatList, NativeModules, TextInput, View } from "react-native";
import { Button, IconButton, ProgressBar } from 'react-native-paper' import { Button, IconButton, ProgressBar } from "react-native-paper";
import AppInput from './AppInput' import AppInput from "./AppInput";
import { getBestSet } from './best.service' import { getBestSet } from "./best.service";
import { PADDING } from './constants' import { PADDING } from "./constants";
import CountMany from './count-many' import CountMany from "./count-many";
import { AppDataSource } from './data-source' import { AppDataSource } from "./data-source";
import { getNow, setRepo, settingsRepo } from './db' import { getNow, setRepo, settingsRepo } from "./db";
import GymSet from './gym-set' import GymSet from "./gym-set";
import { PlanPageParams } from './plan-page-params' import { PlanPageParams } from "./plan-page-params";
import Settings from './settings' import Settings from "./settings";
import StackHeader from './StackHeader' import StackHeader from "./StackHeader";
import StartPlanItem from './StartPlanItem' import StartPlanItem from "./StartPlanItem";
import { toast } from './toast' 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 [reps, setReps] = useState(params.first?.reps.toString() || '0') const [reps, setReps] = useState(params.first?.reps.toString() || "0");
const [weight, setWeight] = useState(params.first?.weight.toString() || '0') const [weight, setWeight] = useState(params.first?.weight.toString() || "0");
const [unit, setUnit] = useState<string>(params.first?.unit || 'kg') const [unit, setUnit] = useState<string>(params.first?.unit || "kg");
const [selected, setSelected] = useState(0) const [selected, setSelected] = useState(0);
const [settings, setSettings] = useState<Settings>() const [settings, setSettings] = useState<Settings>();
const [counts, setCounts] = useState<CountMany[]>() const [counts, setCounts] = useState<CountMany[]>();
const weightRef = useRef<TextInput>(null) const weightRef = useRef<TextInput>(null);
const repsRef = useRef<TextInput>(null) const repsRef = useRef<TextInput>(null);
const unitRef = useRef<TextInput>(null) const unitRef = useRef<TextInput>(null);
const workouts = useMemo(() => params.plan.workouts.split(','), [params]) const workouts = useMemo(() => params.plan.workouts.split(","), [params]);
const navigation = useNavigation<NavigationProp<PlanPageParams>>() const navigation = useNavigation<NavigationProp<PlanPageParams>>();
const [selection, setSelection] = useState({ const [selection, setSelection] = useState({
start: 0, start: 0,
end: 0, end: 0,
}) });
const refresh = useCallback(async () => { const refresh = useCallback(async () => {
const questions = workouts const questions = workouts
.map((workout, index) => `('${workout}',${index})`) .map((workout, index) => `('${workout}',${index})`)
.join(',') .join(",");
const select = ` const select = `
SELECT workouts.name, COUNT(sets.id) as total, sets.sets SELECT workouts.name, COUNT(sets.id) as total, sets.sets
FROM (select 0 as name, 0 as sequence union values ${questions}) as workouts FROM (select 0 as name, 0 as sequence union values ${questions}) as workouts
@ -54,45 +54,45 @@ export default function StartPlan() {
ORDER BY workouts.sequence ORDER BY workouts.sequence
LIMIT -1 LIMIT -1
OFFSET 1 OFFSET 1
` `;
const newCounts = await AppDataSource.manager.query(select) const newCounts = await AppDataSource.manager.query(select);
console.log(`${StartPlan.name}.focus:`, { newCounts }) console.log(`${StartPlan.name}.focus:`, { newCounts });
setCounts(newCounts) setCounts(newCounts);
}, [workouts]) }, [workouts]);
const select = useCallback( const select = useCallback(
async (index: number, newCounts?: CountMany[]) => { async (index: number, newCounts?: CountMany[]) => {
setSelected(index) setSelected(index);
if (!counts && !newCounts) return if (!counts && !newCounts) return;
const workout = counts ? counts[index] : newCounts[index] const workout = counts ? counts[index] : newCounts[index];
console.log(`${StartPlan.name}.next:`, { workout }) console.log(`${StartPlan.name}.next:`, { workout });
const last = await setRepo.findOne({ const last = await setRepo.findOne({
where: { name: workout.name }, where: { name: workout.name },
order: { created: 'desc' }, order: { created: "desc" },
}) });
console.log({ last }) console.log({ last });
if (!last) return if (!last) return;
delete last.id delete last.id;
console.log(`${StartPlan.name}.select:`, { last }) console.log(`${StartPlan.name}.select:`, { last });
setReps(last.reps.toString()) setReps(last.reps.toString());
setWeight(last.weight.toString()) setWeight(last.weight.toString());
setUnit(last.unit) setUnit(last.unit);
}, },
[counts], [counts]
) );
useFocusEffect( useFocusEffect(
useCallback(() => { useCallback(() => {
settingsRepo.findOne({ where: {} }).then(setSettings) settingsRepo.findOne({ where: {} }).then(setSettings);
refresh() refresh();
}, [refresh]), }, [refresh])
) );
const handleSubmit = async () => { const handleSubmit = async () => {
const now = await getNow() const now = await getNow();
const workout = counts[selected] const workout = counts[selected];
const best = await getBestSet(workout.name) const best = await getBestSet(workout.name);
delete best.id delete best.id;
const newSet: GymSet = { const newSet: GymSet = {
...best, ...best,
weight: +weight, weight: +weight,
@ -100,34 +100,34 @@ export default function StartPlan() {
unit, unit,
created: now, created: now,
hidden: false, hidden: false,
} };
await setRepo.save(newSet) await setRepo.save(newSet);
await refresh() await refresh();
if ( if (
settings.notify && settings.notify &&
(+weight > best.weight || (+reps > best.reps && +weight === best.weight)) (+weight > best.weight || (+reps > best.reps && +weight === best.weight))
) { ) {
toast("Great work King! That's a new record.") toast("Great work King! That's a new record.");
} }
if (!settings.alarm) return if (!settings.alarm) return;
const milliseconds = Number(best.minutes) * 60 * 1000 + const milliseconds =
Number(best.seconds) * 1000 Number(best.minutes) * 60 * 1000 + Number(best.seconds) * 1000;
NativeModules.AlarmModule.timer(milliseconds) NativeModules.AlarmModule.timer(milliseconds);
} };
return ( return (
<> <>
<StackHeader title={params.plan.days.replace(/,/g, ', ')}> <StackHeader title={params.plan.days.replace(/,/g, ", ")}>
<IconButton <IconButton
onPress={() => navigation.navigate('EditPlan', { plan: params.plan })} onPress={() => navigation.navigate("EditPlan", { plan: params.plan })}
icon='edit' icon="edit"
/> />
</StackHeader> </StackHeader>
<View style={{ padding: PADDING, flex: 1, flexDirection: 'column' }}> <View style={{ padding: PADDING, flex: 1, flexDirection: "column" }}>
<View style={{ flex: 1 }}> <View style={{ flex: 1 }}>
<AppInput <AppInput
label='Reps' label="Reps"
keyboardType='numeric' keyboardType="numeric"
value={reps} value={reps}
onChangeText={setReps} onChangeText={setReps}
onSubmitEditing={() => weightRef.current?.focus()} onSubmitEditing={() => weightRef.current?.focus()}
@ -136,8 +136,8 @@ export default function StartPlan() {
innerRef={repsRef} innerRef={repsRef}
/> />
<AppInput <AppInput
label='Weight' label="Weight"
keyboardType='numeric' keyboardType="numeric"
value={weight} value={weight}
onChangeText={setWeight} onChangeText={setWeight}
onSubmitEditing={handleSubmit} onSubmitEditing={handleSubmit}
@ -146,8 +146,8 @@ export default function StartPlan() {
/> />
{settings?.showUnit && ( {settings?.showUnit && (
<AppInput <AppInput
autoCapitalize='none' autoCapitalize="none"
label='Unit' label="Unit"
value={unit} value={unit}
onChangeText={setUnit} onChangeText={setUnit}
innerRef={unitRef} innerRef={unitRef}
@ -172,10 +172,10 @@ export default function StartPlan() {
/> />
)} )}
</View> </View>
<Button mode='outlined' icon='save' onPress={handleSubmit}> <Button mode="outlined" icon="save" onPress={handleSubmit}>
Save Save
</Button> </Button>
</View> </View>
</> </>
) );
} }

View File

@ -1,109 +1,112 @@
import { NavigationProp, useNavigation } from '@react-navigation/native' import { NavigationProp, useNavigation } from "@react-navigation/native";
import React, { useCallback, useState } from 'react' import React, { useCallback, useState } from "react";
import { GestureResponderEvent, ListRenderItemInfo, View } from 'react-native' import { GestureResponderEvent, ListRenderItemInfo, View } from "react-native";
import { List, Menu, RadioButton, useTheme } from 'react-native-paper' import { List, Menu, RadioButton, useTheme } from "react-native-paper";
import { Like } from 'typeorm' import { Like } from "typeorm";
import CountMany from './count-many' import CountMany from "./count-many";
import { getNow, setRepo } from './db' import { getNow, setRepo } from "./db";
import { PlanPageParams } from './plan-page-params' import { PlanPageParams } from "./plan-page-params";
import { toast } from './toast' import { toast } from "./toast";
interface Props extends ListRenderItemInfo<CountMany> { interface Props extends ListRenderItemInfo<CountMany> {
onSelect: (index: number) => void onSelect: (index: number) => void;
selected: number selected: number;
onUndo: () => void onUndo: () => void;
} }
export default function StartPlanItem(props: Props) { export default function StartPlanItem(props: Props) {
const { index, item, onSelect, selected, onUndo } = props const { index, item, onSelect, selected, onUndo } = props;
const { colors } = useTheme() const { colors } = useTheme();
const [anchor, setAnchor] = useState({ x: 0, y: 0 }) const [anchor, setAnchor] = useState({ x: 0, y: 0 });
const [showMenu, setShowMenu] = useState(false) const [showMenu, setShowMenu] = useState(false);
const { navigate } = useNavigation<NavigationProp<PlanPageParams>>() const { navigate } = useNavigation<NavigationProp<PlanPageParams>>();
const undo = useCallback(async () => { const undo = useCallback(async () => {
const now = await getNow() const now = await getNow();
const created = now.split('T')[0] const created = now.split("T")[0];
const first = await setRepo.findOne({ const first = await setRepo.findOne({
where: { where: {
name: item.name, name: item.name,
hidden: 0 as any, hidden: 0 as any,
created: Like(`${created}%`), created: Like(`${created}%`),
}, },
order: { created: 'desc' }, order: { created: "desc" },
}) });
setShowMenu(false) setShowMenu(false);
if (!first) return toast('Nothing to undo.') if (!first) return toast("Nothing to undo.");
await setRepo.delete(first.id) await setRepo.delete(first.id);
onUndo() onUndo();
}, [setShowMenu, onUndo, item.name]) }, [setShowMenu, onUndo, item.name]);
const longPress = useCallback( const longPress = useCallback(
(e: GestureResponderEvent) => { (e: GestureResponderEvent) => {
setAnchor({ x: e.nativeEvent.pageX, y: e.nativeEvent.pageY }) setAnchor({ x: e.nativeEvent.pageX, y: e.nativeEvent.pageY });
setShowMenu(true) setShowMenu(true);
}, },
[setShowMenu, setAnchor], [setShowMenu, setAnchor]
) );
const edit = useCallback(async () => { const edit = useCallback(async () => {
const now = await getNow() const now = await getNow();
const created = now.split('T')[0] const created = now.split("T")[0];
const first = await setRepo.findOne({ const first = await setRepo.findOne({
where: { where: {
name: item.name, name: item.name,
hidden: 0 as any, hidden: 0 as any,
created: Like(`${created}%`), created: Like(`${created}%`),
}, },
order: { created: 'desc' }, order: { created: "desc" },
}) });
setShowMenu(false) setShowMenu(false);
if (!first) return toast('Nothing to edit.') if (!first) return toast("Nothing to edit.");
navigate('EditSet', { set: first }) navigate("EditSet", { set: first });
}, [item.name, navigate]) }, [item.name, navigate]);
const left = useCallback( const left = useCallback(
() => ( () => (
<View style={{ alignItems: 'center', justifyContent: 'center' }}> <View style={{ alignItems: "center", justifyContent: "center" }}>
<RadioButton <RadioButton
onPress={() => onSelect(index)} onPress={() => onSelect(index)}
value={index.toString()} value={index.toString()}
status={selected === index ? 'checked' : 'unchecked'} status={selected === index ? "checked" : "unchecked"}
color={colors.primary} color={colors.primary}
/> />
</View> </View>
), ),
[index, selected, colors.primary, onSelect], [index, selected, colors.primary, onSelect]
) );
const right = useCallback(() => ( const right = useCallback(
<View () => (
style={{ <View
width: '25%', style={{
justifyContent: 'center', width: "25%",
}} justifyContent: "center",
> }}
<Menu
anchor={anchor}
visible={showMenu}
onDismiss={() => setShowMenu(false)}
> >
<Menu.Item leadingIcon='edit' onPress={edit} title='Edit' /> <Menu
<Menu.Item leadingIcon='undo' onPress={undo} title='Undo' /> anchor={anchor}
</Menu> visible={showMenu}
</View> onDismiss={() => setShowMenu(false)}
), [anchor, showMenu, edit, undo]) >
<Menu.Item leadingIcon="edit" onPress={edit} title="Edit" />
<Menu.Item leadingIcon="undo" onPress={undo} title="Undo" />
</Menu>
</View>
),
[anchor, showMenu, edit, undo]
);
return ( return (
<List.Item <List.Item
onLongPress={longPress} onLongPress={longPress}
title={item.name} title={item.name}
description={item.sets description={
? `${item.total} / ${item.sets}` item.sets ? `${item.total} / ${item.sets}` : item.total.toString()
: item.total.toString()} }
onPress={() => onSelect(index)} onPress={() => onSelect(index)}
left={left} left={left}
right={right} right={right}
/> />
) );
} }

View File

@ -1,27 +1,27 @@
import React from 'react' import React from "react";
import { Platform, Pressable } from 'react-native' import { Platform, Pressable } from "react-native";
import { Switch as PaperSwitch, Text, useTheme } from 'react-native-paper' import { Switch as PaperSwitch, Text, useTheme } from "react-native-paper";
import { MARGIN } from './constants' import { MARGIN } from "./constants";
function Switch({ function Switch({
value, value,
onChange, onChange,
title, title,
}: { }: {
value?: boolean value?: boolean;
onChange: (value: boolean) => void onChange: (value: boolean) => void;
title: string title: string;
}) { }) {
const { colors } = useTheme() const { colors } = useTheme();
return ( return (
<Pressable <Pressable
onPress={() => onChange(!value)} onPress={() => onChange(!value)}
style={{ style={{
flexDirection: 'row', flexDirection: "row",
flexWrap: 'wrap', flexWrap: "wrap",
alignItems: 'center', alignItems: "center",
marginBottom: Platform.OS === 'ios' ? MARGIN : null, marginBottom: Platform.OS === "ios" ? MARGIN : null,
}} }}
> >
<PaperSwitch <PaperSwitch
@ -30,13 +30,13 @@ function Switch({
value={value} value={value}
onValueChange={onChange} onValueChange={onChange}
trackColor={{ trackColor={{
true: colors.primary + '80', true: colors.primary + "80",
false: colors.surfaceDisabled, false: colors.surfaceDisabled,
}} }}
/> />
<Text>{title}</Text> <Text>{title}</Text>
</Pressable> </Pressable>
) );
} }
export default React.memo(Switch) export default React.memo(Switch);

View File

@ -1,60 +1,60 @@
import { useFocusEffect } from '@react-navigation/native' import { useFocusEffect } from "@react-navigation/native";
import React, { useCallback, useMemo, useState } from 'react' import React, { useCallback, useMemo, useState } from "react";
import { Dimensions, NativeModules, View } from 'react-native' import { Dimensions, NativeModules, View } from "react-native";
import { Button, Text, useTheme } from 'react-native-paper' import { Button, Text, useTheme } from "react-native-paper";
import { ProgressCircle } from 'react-native-svg-charts' import { ProgressCircle } from "react-native-svg-charts";
import AppFab from './AppFab' import AppFab from "./AppFab";
import { MARGIN, PADDING } from './constants' import { MARGIN, PADDING } from "./constants";
import { settingsRepo } from './db' import { settingsRepo } from "./db";
import DrawerHeader from './DrawerHeader' import DrawerHeader from "./DrawerHeader";
import Settings from './settings' import Settings from "./settings";
import useTimer from './use-timer' import useTimer from "./use-timer";
export interface TickEvent { export interface TickEvent {
minutes: string minutes: string;
seconds: string seconds: string;
} }
export default function TimerPage() { export default function TimerPage() {
const { minutes, seconds } = useTimer() const { minutes, seconds } = useTimer();
const [settings, setSettings] = useState<Settings>() const [settings, setSettings] = useState<Settings>();
const { colors } = useTheme() const { colors } = useTheme();
useFocusEffect( useFocusEffect(
useCallback(() => { useCallback(() => {
settingsRepo.findOne({ where: {} }).then(setSettings) settingsRepo.findOne({ where: {} }).then(setSettings);
}, []), }, [])
) );
const stop = () => { const stop = () => {
NativeModules.AlarmModule.stop() NativeModules.AlarmModule.stop();
} };
const add = async () => { const add = async () => {
console.log(`${TimerPage.name}.add:`, settings) console.log(`${TimerPage.name}.add:`, settings);
NativeModules.AlarmModule.add() NativeModules.AlarmModule.add();
} };
const progress = useMemo(() => { const progress = useMemo(() => {
return (Number(minutes) * 60 + Number(seconds)) / 210 return (Number(minutes) * 60 + Number(seconds)) / 210;
}, [minutes, seconds]) }, [minutes, seconds]);
const left = useMemo(() => { const left = useMemo(() => {
return Dimensions.get('screen').width * 0.5 - 60 return Dimensions.get("screen").width * 0.5 - 60;
}, []) }, []);
return ( return (
<> <>
<DrawerHeader name='Timer' /> <DrawerHeader name="Timer" />
<View style={{ flexGrow: 1, padding: PADDING }}> <View style={{ flexGrow: 1, padding: PADDING }}>
<View <View
style={{ style={{
flex: 1, flex: 1,
justifyContent: 'center', justifyContent: "center",
alignItems: 'center', alignItems: "center",
}} }}
> >
<Text style={{ fontSize: 70, position: 'absolute' }}> <Text style={{ fontSize: 70, position: "absolute" }}>
{minutes}:{seconds} {minutes}:{seconds}
</Text> </Text>
<ProgressCircle <ProgressCircle
@ -62,14 +62,14 @@ export default function TimerPage() {
progress={progress} progress={progress}
strokeWidth={10} strokeWidth={10}
progressColor={colors.primary} progressColor={colors.primary}
backgroundColor={colors.primary + '80'} backgroundColor={colors.primary + "80"}
/> />
</View> </View>
</View> </View>
<Button onPress={add} style={{ position: 'absolute', top: '82%', left }}> <Button onPress={add} style={{ position: "absolute", top: "82%", left }}>
Add 1 min Add 1 min
</Button> </Button>
<AppFab icon='stop' onPress={stop} /> <AppFab icon="stop" onPress={stop} />
</> </>
) );
} }

View File

@ -1,73 +1,73 @@
import { RouteProp, useRoute } from '@react-navigation/native' import { RouteProp, useRoute } from "@react-navigation/native";
import { format } from 'date-fns' import { format } from "date-fns";
import { useEffect, useMemo, useState } from 'react' import { useEffect, useMemo, useState } from "react";
import { View } from 'react-native' import { View } from "react-native";
import { FileSystem } from 'react-native-file-access' import { FileSystem } from "react-native-file-access";
import { IconButton, List } from 'react-native-paper' import { IconButton, List } from "react-native-paper";
import Share from 'react-native-share' import Share from "react-native-share";
import { captureScreen } from 'react-native-view-shot' import { captureScreen } from "react-native-view-shot";
import Chart from './Chart' import Chart from "./Chart";
import { GraphsPageParams } from './GraphsPage' import { GraphsPageParams } from "./GraphsPage";
import Select from './Select' import Select from "./Select";
import StackHeader from './StackHeader' import StackHeader from "./StackHeader";
import { PADDING } from './constants' import { PADDING } from "./constants";
import { setRepo } from './db' import { setRepo } from "./db";
import GymSet from './gym-set' import GymSet from "./gym-set";
import { Metrics } from './metrics' import { Metrics } from "./metrics";
import { Periods } from './periods' import { Periods } from "./periods";
import Volume from './volume' import Volume from "./volume";
export default function ViewGraph() { export default function ViewGraph() {
const { params } = useRoute<RouteProp<GraphsPageParams, 'ViewGraph'>>() const { params } = useRoute<RouteProp<GraphsPageParams, "ViewGraph">>();
const [weights, setWeights] = useState<GymSet[]>() const [weights, setWeights] = useState<GymSet[]>();
const [volumes, setVolumes] = useState<Volume[]>() const [volumes, setVolumes] = useState<Volume[]>();
const [metric, setMetric] = useState(Metrics.Weight) const [metric, setMetric] = useState(Metrics.Weight);
const [period, setPeriod] = useState(Periods.Monthly) const [period, setPeriod] = useState(Periods.Monthly);
useEffect(() => { useEffect(() => {
let difference = '-7 days' let difference = "-7 days";
if (period === Periods.Monthly) difference = '-1 months' if (period === Periods.Monthly) difference = "-1 months";
else if (period === Periods.Yearly) difference = '-1 years' else if (period === Periods.Yearly) difference = "-1 years";
let group = '%Y-%m-%d' let group = "%Y-%m-%d";
if (period === Periods.Yearly) group = '%Y-%m' if (period === Periods.Yearly) group = "%Y-%m";
const builder = setRepo const builder = setRepo
.createQueryBuilder() .createQueryBuilder()
.select("STRFTIME('%Y-%m-%d', created)", 'created') .select("STRFTIME('%Y-%m-%d', created)", "created")
.addSelect('unit') .addSelect("unit")
.where('name = :name', { name: params.best.name }) .where("name = :name", { name: params.best.name })
.andWhere('NOT hidden') .andWhere("NOT hidden")
.andWhere("DATE(created) >= DATE('now', 'weekday 0', :difference)", { .andWhere("DATE(created) >= DATE('now', 'weekday 0', :difference)", {
difference, difference,
}) })
.groupBy('name') .groupBy("name")
.addGroupBy(`STRFTIME('${group}', created)`) .addGroupBy(`STRFTIME('${group}', created)`);
switch (metric) { switch (metric) {
case Metrics.Weight: case Metrics.Weight:
builder builder
.addSelect('ROUND(MAX(weight), 2)', 'weight') .addSelect("ROUND(MAX(weight), 2)", "weight")
.getRawMany() .getRawMany()
.then(setWeights) .then(setWeights);
break break;
case Metrics.Volume: case Metrics.Volume:
builder builder
.addSelect('ROUND(SUM(weight * reps), 2)', 'value') .addSelect("ROUND(SUM(weight * reps), 2)", "value")
.getRawMany() .getRawMany()
.then(setVolumes) .then(setVolumes);
break break;
default: default:
// Brzycki formula https://en.wikipedia.org/wiki/One-repetition_maximum#Brzycki // Brzycki formula https://en.wikipedia.org/wiki/One-repetition_maximum#Brzycki
builder builder
.addSelect( .addSelect(
'ROUND(MAX(weight / (1.0278 - 0.0278 * reps)), 2)', "ROUND(MAX(weight / (1.0278 - 0.0278 * reps)), 2)",
'weight', "weight"
) )
.getRawMany() .getRawMany()
.then((newWeights) => { .then((newWeights) => {
console.log({ weights: newWeights }) console.log({ weights: newWeights });
setWeights(newWeights) setWeights(newWeights);
}) });
} }
}, [params.best.name, metric, period]) }, [params.best.name, metric, period]);
const charts = useMemo(() => { const charts = useMemo(() => {
if ( if (
@ -75,21 +75,23 @@ export default function ViewGraph() {
(metric === Metrics.Weight && weights?.length === 0) || (metric === Metrics.Weight && weights?.length === 0) ||
(metric === Metrics.OneRepMax && weights?.length === 0) (metric === Metrics.OneRepMax && weights?.length === 0)
) { ) {
return <List.Item title='No data yet.' /> return <List.Item title="No data yet." />;
} }
if (metric === Metrics.Volume && volumes?.length && weights?.length) { if (metric === Metrics.Volume && volumes?.length && weights?.length) {
return ( return (
<Chart <Chart
yData={volumes.map((v) => v.value)} yData={volumes.map((v) => v.value)}
yFormat={(value: number) => yFormat={(value: number) =>
`${value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')}${ `${value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",")}${
volumes[0].unit || 'kg' volumes[0].unit || "kg"
}`} }`
}
xData={weights} xData={weights}
xFormat={(_value, index) => xFormat={(_value, index) =>
format(new Date(weights[index].created), 'd/M')} format(new Date(weights[index].created), "d/M")
}
/> />
) );
} }
return ( return (
@ -98,10 +100,11 @@ export default function ViewGraph() {
yFormat={(value) => `${value}${weights?.[0].unit}`} yFormat={(value) => `${value}${weights?.[0].unit}`}
xData={weights || []} xData={weights || []}
xFormat={(_value, index) => xFormat={(_value, index) =>
format(new Date(weights?.[index].created), 'd/M')} format(new Date(weights?.[index].created), "d/M")
}
/> />
) );
}, [volumes, weights, metric]) }, [volumes, weights, metric]);
return ( return (
<> <>
@ -109,19 +112,20 @@ export default function ViewGraph() {
<IconButton <IconButton
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"
/> />
</StackHeader> </StackHeader>
<View style={{ padding: PADDING }}> <View style={{ padding: PADDING }}>
<Select <Select
label='Metric' label="Metric"
items={[ items={[
{ value: Metrics.Volume, label: Metrics.Volume }, { value: Metrics.Volume, label: Metrics.Volume },
{ value: Metrics.OneRepMax, label: Metrics.OneRepMax }, { value: Metrics.OneRepMax, label: Metrics.OneRepMax },
@ -134,7 +138,7 @@ export default function ViewGraph() {
value={metric} value={metric}
/> />
<Select <Select
label='Period' label="Period"
items={[ items={[
{ value: Periods.Weekly, label: Periods.Weekly }, { value: Periods.Weekly, label: Periods.Weekly },
{ value: Periods.Monthly, label: Periods.Monthly }, { value: Periods.Monthly, label: Periods.Monthly },
@ -146,5 +150,5 @@ export default function ViewGraph() {
{charts} {charts}
</View> </View>
</> </>
) );
} }

View File

@ -1,60 +1,57 @@
import { NavigationProp, useNavigation } from '@react-navigation/native' import { NavigationProp, useNavigation } from "@react-navigation/native";
import { useCallback, useMemo, useState } from 'react' import { useCallback, useMemo, useState } from "react";
import { GestureResponderEvent, Image } from 'react-native' import { GestureResponderEvent, Image } from "react-native";
import { List, Menu, Text } from 'react-native-paper' import { List, Menu, Text } from "react-native-paper";
import ConfirmDialog from './ConfirmDialog' import ConfirmDialog from "./ConfirmDialog";
import { setRepo } from './db' import { setRepo } from "./db";
import GymSet from './gym-set' import GymSet from "./gym-set";
import { WorkoutsPageParams } from './WorkoutsPage' import { WorkoutsPageParams } from "./WorkoutsPage";
export default function WorkoutItem({ export default function WorkoutItem({
item, item,
onRemove, onRemove,
images, images,
}: { }: {
item: GymSet item: GymSet;
onRemove: () => void onRemove: () => void;
images: boolean 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 navigation = useNavigation<NavigationProp<WorkoutsPageParams>>() const navigation = useNavigation<NavigationProp<WorkoutsPageParams>>();
const remove = useCallback(async () => { const remove = useCallback(async () => {
await setRepo.delete({ name: item.name }) await setRepo.delete({ name: item.name });
setShowMenu(false) setShowMenu(false);
onRemove() onRemove();
}, [setShowMenu, onRemove, item.name]) }, [setShowMenu, onRemove, item.name]);
const longPress = useCallback( const longPress = useCallback(
(e: GestureResponderEvent) => { (e: GestureResponderEvent) => {
setAnchor({ x: e.nativeEvent.pageX, y: e.nativeEvent.pageY }) setAnchor({ x: e.nativeEvent.pageX, y: e.nativeEvent.pageY });
setShowMenu(true) setShowMenu(true);
}, },
[setShowMenu, setAnchor], [setShowMenu, setAnchor]
) );
const description = useMemo(() => { const description = useMemo(() => {
const seconds = item.seconds?.toString().padStart(2, '0') const seconds = item.seconds?.toString().padStart(2, "0");
return `${item.sets} x ${item.minutes || 0}:${seconds}` return `${item.sets} x ${item.minutes || 0}:${seconds}`;
}, [item]) }, [item]);
const left = useCallback(() => { const left = useCallback(() => {
if (!images || !item.image) return null if (!images || !item.image) return null;
return ( return (
<Image <Image source={{ uri: item.image }} style={{ height: 75, width: 75 }} />
source={{ uri: item.image }} );
style={{ height: 75, width: 75 }} }, [item.image, images]);
/>
)
}, [item.image, images])
const right = useCallback(() => { const right = useCallback(() => {
return ( return (
<Text <Text
style={{ style={{
alignSelf: 'center', alignSelf: "center",
}} }}
> >
<Menu <Menu
@ -63,22 +60,22 @@ export default function WorkoutItem({
onDismiss={() => setShowMenu(false)} onDismiss={() => setShowMenu(false)}
> >
<Menu.Item <Menu.Item
leadingIcon='delete' leadingIcon="delete"
onPress={() => { onPress={() => {
setShowRemove(item.name) setShowRemove(item.name);
setShowMenu(false) setShowMenu(false);
}} }}
title='Delete' title="Delete"
/> />
</Menu> </Menu>
</Text> </Text>
) );
}, [anchor, showMenu, item.name]) }, [anchor, showMenu, item.name]);
return ( return (
<> <>
<List.Item <List.Item
onPress={() => navigation.navigate('EditWorkout', { value: item })} onPress={() => navigation.navigate("EditWorkout", { value: item })}
title={item.name} title={item.name}
description={description} description={description}
onLongPress={longPress} onLongPress={longPress}
@ -88,12 +85,12 @@ export default function WorkoutItem({
<ConfirmDialog <ConfirmDialog
title={`Delete ${showRemove}`} title={`Delete ${showRemove}`}
show={!!showRemove} show={!!showRemove}
setShow={(show) => (show ? null : setShowRemove(''))} setShow={(show) => (show ? null : setShowRemove(""))}
onOk={remove} onOk={remove}
> >
This irreversibly deletes ALL sets related to this workout. Are you This irreversibly deletes ALL sets related to this workout. Are you
sure? sure?
</ConfirmDialog> </ConfirmDialog>
</> </>
) );
} }

View File

@ -2,50 +2,50 @@ import {
NavigationProp, NavigationProp,
useFocusEffect, useFocusEffect,
useNavigation, useNavigation,
} from '@react-navigation/native' } from "@react-navigation/native";
import { useCallback, useState } from 'react' import { useCallback, useState } from "react";
import { FlatList } from 'react-native' import { FlatList } from "react-native";
import { List } from 'react-native-paper' import { List } from "react-native-paper";
import { setRepo, settingsRepo } from './db' import { setRepo, settingsRepo } from "./db";
import DrawerHeader from './DrawerHeader' import DrawerHeader from "./DrawerHeader";
import GymSet from './gym-set' import GymSet from "./gym-set";
import Page from './Page' import Page from "./Page";
import SetList from './SetList' import SetList from "./SetList";
import Settings from './settings' import Settings from "./settings";
import WorkoutItem from './WorkoutItem' import WorkoutItem from "./WorkoutItem";
import { WorkoutsPageParams } from './WorkoutsPage' import { WorkoutsPageParams } from "./WorkoutsPage";
const limit = 15 const limit = 15;
export default function WorkoutList() { export default function WorkoutList() {
const [workouts, setWorkouts] = useState<GymSet[]>() const [workouts, setWorkouts] = useState<GymSet[]>();
const [offset, setOffset] = useState(0) const [offset, setOffset] = useState(0);
const [term, setTerm] = useState('') const [term, setTerm] = useState("");
const [end, setEnd] = useState(false) const [end, setEnd] = useState(false);
const [settings, setSettings] = useState<Settings>() const [settings, setSettings] = useState<Settings>();
const navigation = useNavigation<NavigationProp<WorkoutsPageParams>>() const navigation = useNavigation<NavigationProp<WorkoutsPageParams>>();
const refresh = useCallback(async (value: string) => { const refresh = useCallback(async (value: string) => {
const newWorkouts = await setRepo const newWorkouts = await setRepo
.createQueryBuilder() .createQueryBuilder()
.select() .select()
.where('name LIKE :name', { name: `%${value.trim()}%` }) .where("name LIKE :name", { name: `%${value.trim()}%` })
.groupBy('name') .groupBy("name")
.orderBy('name') .orderBy("name")
.limit(limit) .limit(limit)
.getMany() .getMany();
console.log(`${WorkoutList.name}`, { newWorkout: newWorkouts[0] }) console.log(`${WorkoutList.name}`, { newWorkout: newWorkouts[0] });
setWorkouts(newWorkouts) setWorkouts(newWorkouts);
setOffset(0) setOffset(0);
setEnd(false) setEnd(false);
}, []) }, []);
useFocusEffect( useFocusEffect(
useCallback(() => { useCallback(() => {
refresh(term) refresh(term);
settingsRepo.findOne({ where: {} }).then(setSettings) settingsRepo.findOne({ where: {} }).then(setSettings);
}, [refresh, term]), }, [refresh, term])
) );
const renderItem = useCallback( const renderItem = useCallback(
({ item }: { item: GymSet }) => ( ({ item }: { item: GymSet }) => (
@ -56,69 +56,67 @@ export default function WorkoutList() {
onRemove={() => refresh(term)} onRemove={() => refresh(term)}
/> />
), ),
[refresh, term, settings?.images], [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,
term, term,
}) });
const newWorkouts = await setRepo const newWorkouts = await setRepo
.createQueryBuilder() .createQueryBuilder()
.select() .select()
.where('name LIKE :name', { name: `%${term.trim()}%` }) .where("name LIKE :name", { name: `%${term.trim()}%` })
.groupBy('name') .groupBy("name")
.orderBy('name') .orderBy("name")
.limit(limit) .limit(limit)
.offset(newOffset) .offset(newOffset)
.getMany() .getMany();
if (newWorkouts.length === 0) return setEnd(true) if (newWorkouts.length === 0) return setEnd(true);
if (!workouts) return if (!workouts) return;
setWorkouts([...workouts, ...newWorkouts]) setWorkouts([...workouts, ...newWorkouts]);
if (newWorkouts.length < limit) return setEnd(true) if (newWorkouts.length < limit) return setEnd(true);
setOffset(newOffset) setOffset(newOffset);
}, [term, end, offset, workouts]) }, [term, end, offset, workouts]);
const onAdd = useCallback(async () => { const onAdd = useCallback(async () => {
navigation.navigate('EditWorkout', { navigation.navigate("EditWorkout", {
value: new GymSet(), value: new GymSet(),
}) });
}, [navigation]) }, [navigation]);
const search = useCallback( const search = useCallback(
(value: string) => { (value: string) => {
setTerm(value) setTerm(value);
refresh(value) refresh(value);
}, },
[refresh], [refresh]
) );
return ( return (
<> <>
<DrawerHeader name='Workouts' /> <DrawerHeader name="Workouts" />
<Page onAdd={onAdd} term={term} search={search}> <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.' description="A workout is something you do at the gym. For example Deadlifts are a workout."
description='A workout is something you do at the gym. For example Deadlifts are a workout.' />
/> ) : (
) <FlatList
: ( data={workouts}
<FlatList style={{ flex: 1 }}
data={workouts} renderItem={renderItem}
style={{ flex: 1 }} keyExtractor={(w) => w.name}
renderItem={renderItem} onEndReached={next}
keyExtractor={(w) => w.name} />
onEndReached={next} )}
/>
)}
</Page> </Page>
</> </>
) );
} }

View File

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

View File

@ -1,15 +1,15 @@
import { setRepo } from './db' import { setRepo } from "./db";
import GymSet from './gym-set' import GymSet from "./gym-set";
export const getBestSet = async (name: string): Promise<GymSet> => { export const getBestSet = async (name: string): Promise<GymSet> => {
return setRepo return setRepo
.createQueryBuilder() .createQueryBuilder()
.select() .select()
.addSelect('MAX(weight)', 'weight') .addSelect("MAX(weight)", "weight")
.where('name = :name', { name }) .where("name = :name", { name })
.groupBy('name') .groupBy("name")
.addGroupBy('reps') .addGroupBy("reps")
.orderBy('weight', 'DESC') .orderBy("weight", "DESC")
.addOrderBy('reps', 'DESC') .addOrderBy("reps", "DESC")
.getOne() .getOne();
} };

View File

@ -1,41 +1,41 @@
import { DefaultTheme, MD3DarkTheme } from 'react-native-paper' import { DefaultTheme, MD3DarkTheme } from "react-native-paper";
export const lightColors = [ export const lightColors = [
{ hex: MD3DarkTheme.colors.primary, name: 'Purple' }, { hex: MD3DarkTheme.colors.primary, name: "Purple" },
{ hex: '#B3E5FC', name: 'Blue' }, { hex: "#B3E5FC", name: "Blue" },
{ hex: '#FA8072', name: 'Salmon' }, { hex: "#FA8072", name: "Salmon" },
{ hex: '#FFC0CB', name: 'Pink' }, { hex: "#FFC0CB", name: "Pink" },
{ hex: '#E9DCC9', name: 'Linen' }, { hex: "#E9DCC9", name: "Linen" },
] ];
export const darkColors = [ export const darkColors = [
{ hex: DefaultTheme.colors.primary, name: 'Purple' }, { hex: DefaultTheme.colors.primary, name: "Purple" },
{ hex: '#0051a9', name: 'Blue' }, { hex: "#0051a9", name: "Blue" },
{ hex: '#000000', name: 'Black' }, { hex: "#000000", name: "Black" },
{ hex: '#863c3c', name: 'Red' }, { hex: "#863c3c", name: "Red" },
{ hex: '#1c6000', name: 'Kermit' }, { hex: "#1c6000", name: "Kermit" },
] ];
export const colorShade = (color: any, amount: number) => { export const colorShade = (color: any, amount: number) => {
color = color.replace(/^#/, '') color = color.replace(/^#/, "");
if (color.length === 3) { if (color.length === 3) {
color = color[0] + color[0] + color[1] + color[1] + color[2] + color[2] color = color[0] + color[0] + color[1] + color[1] + color[2] + color[2];
} }
let [r, g, b] = color.match(/.{2}/g) let [r, g, b] = color.match(/.{2}/g);
;[r, g, b] = [ [r, g, b] = [
parseInt(r, 16) + amount, parseInt(r, 16) + amount,
parseInt(g, 16) + amount, parseInt(g, 16) + amount,
parseInt(b, 16) + amount, parseInt(b, 16) + amount,
] ];
r = Math.max(Math.min(255, r), 0).toString(16) r = Math.max(Math.min(255, r), 0).toString(16);
g = Math.max(Math.min(255, g), 0).toString(16) g = Math.max(Math.min(255, g), 0).toString(16);
b = Math.max(Math.min(255, b), 0).toString(16) b = Math.max(Math.min(255, b), 0).toString(16);
const rr = (r.length < 2 ? '0' : '') + r const rr = (r.length < 2 ? "0" : "") + r;
const gg = (g.length < 2 ? '0' : '') + g const gg = (g.length < 2 ? "0" : "") + g;
const bb = (b.length < 2 ? '0' : '') + b const bb = (b.length < 2 ? "0" : "") + b;
return `#${rr}${gg}${bb}` return `#${rr}${gg}${bb}`;
} };

View File

@ -1,5 +1,5 @@
export const MARGIN = 10 export const MARGIN = 10;
export const PADDING = 10 export const PADDING = 10;
export const ITEM_PADDING = 8 export const ITEM_PADDING = 8;
export const DARK_RIPPLE = '#444444' export const DARK_RIPPLE = "#444444";
export const LIGHT_RIPPLE = '#c2c2c2' export const LIGHT_RIPPLE = "#c2c2c2";

View File

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

View File

@ -1,40 +1,40 @@
import { DataSource } from 'typeorm' import { DataSource } from "typeorm";
import GymSet from './gym-set' import GymSet from "./gym-set";
import { Sets1667185586014 as sets1667185586014 } from './migrations/1667185586014-sets' import { Sets1667185586014 as sets1667185586014 } from "./migrations/1667185586014-sets";
import { plans1667186124792 } from './migrations/1667186124792-plans' import { plans1667186124792 } from "./migrations/1667186124792-plans";
import { settings1667186130041 } from './migrations/1667186130041-settings' import { settings1667186130041 } from "./migrations/1667186130041-settings";
import { addSound1667186139844 } from './migrations/1667186139844-add-sound' import { addSound1667186139844 } from "./migrations/1667186139844-add-sound";
import { addHidden1667186159379 } from './migrations/1667186159379-add-hidden' import { addHidden1667186159379 } from "./migrations/1667186159379-add-hidden";
import { addNotify1667186166140 } from './migrations/1667186166140-add-notify' import { addNotify1667186166140 } from "./migrations/1667186166140-add-notify";
import { addImage1667186171548 } from './migrations/1667186171548-add-image' import { addImage1667186171548 } from "./migrations/1667186171548-add-image";
import { addImages1667186179488 } from './migrations/1667186179488-add-images' import { addImages1667186179488 } from "./migrations/1667186179488-add-images";
import { insertSettings1667186203827 } from './migrations/1667186203827-insert-settings' import { insertSettings1667186203827 } from "./migrations/1667186203827-insert-settings";
import { addSteps1667186211251 } from './migrations/1667186211251-add-steps' import { addSteps1667186211251 } from "./migrations/1667186211251-add-steps";
import { addSets1667186250618 } from './migrations/1667186250618-add-sets' import { addSets1667186250618 } from "./migrations/1667186250618-add-sets";
import { addMinutes1667186255650 } from './migrations/1667186255650-add-minutes' import { addMinutes1667186255650 } from "./migrations/1667186255650-add-minutes";
import { addSeconds1667186259174 } from './migrations/1667186259174-add-seconds' import { addSeconds1667186259174 } from "./migrations/1667186259174-add-seconds";
import { addShowUnit1667186265588 } from './migrations/1667186265588-add-show-unit' import { addShowUnit1667186265588 } from "./migrations/1667186265588-add-show-unit";
import { addColor1667186320954 } from './migrations/1667186320954-add-color' import { addColor1667186320954 } from "./migrations/1667186320954-add-color";
import { addSteps1667186348425 } from './migrations/1667186348425-add-steps' import { addSteps1667186348425 } from "./migrations/1667186348425-add-steps";
import { addDate1667186431804 } from './migrations/1667186431804-add-date' import { addDate1667186431804 } from "./migrations/1667186431804-add-date";
import { addShowDate1667186435051 } from './migrations/1667186435051-add-show-date' import { addShowDate1667186435051 } from "./migrations/1667186435051-add-show-date";
import { addTheme1667186439366 } from './migrations/1667186439366-add-theme' import { addTheme1667186439366 } from "./migrations/1667186439366-add-theme";
import { addShowSets1667186443614 } from './migrations/1667186443614-add-show-sets' import { addShowSets1667186443614 } from "./migrations/1667186443614-add-show-sets";
import { addSetsCreated1667186451005 } from './migrations/1667186451005-add-sets-created' import { addSetsCreated1667186451005 } from "./migrations/1667186451005-add-sets-created";
import { addNoSound1667186456118 } from './migrations/1667186456118-add-no-sound' import { addNoSound1667186456118 } from "./migrations/1667186456118-add-no-sound";
import { dropMigrations1667190214743 } from './migrations/1667190214743-drop-migrations' import { dropMigrations1667190214743 } from "./migrations/1667190214743-drop-migrations";
import { splitColor1669420187764 } from './migrations/1669420187764-split-color' import { splitColor1669420187764 } from "./migrations/1669420187764-split-color";
import { addBackup1678334268359 } from './migrations/1678334268359-add-backup' import { addBackup1678334268359 } from "./migrations/1678334268359-add-backup";
import { Plan } from './plan' import { Plan } from "./plan";
import Settings from './settings' import Settings from "./settings";
export const AppDataSource = new DataSource({ export const AppDataSource = new DataSource({
type: 'react-native', type: "react-native",
database: 'massive.db', database: "massive.db",
location: 'default', location: "default",
entities: [GymSet, Plan, Settings], entities: [GymSet, Plan, Settings],
migrationsRun: true, migrationsRun: true,
migrationsTableName: 'typeorm_migrations', migrationsTableName: "typeorm_migrations",
migrations: [ migrations: [
sets1667185586014, sets1667185586014,
plans1667186124792, plans1667186124792,
@ -62,4 +62,4 @@ export const AppDataSource = new DataSource({
splitColor1669420187764, splitColor1669420187764,
addBackup1678334268359, addBackup1678334268359,
], ],
}) });

22
db.ts
View File

@ -1,15 +1,15 @@
import { AppDataSource } from './data-source' import { AppDataSource } from "./data-source";
import GymSet from './gym-set' import GymSet from "./gym-set";
import { Plan } from './plan' import { Plan } from "./plan";
import Settings from './settings' import Settings from "./settings";
export const setRepo = AppDataSource.manager.getRepository(GymSet) export const setRepo = AppDataSource.manager.getRepository(GymSet);
export const planRepo = AppDataSource.manager.getRepository(Plan) export const planRepo = AppDataSource.manager.getRepository(Plan);
export const settingsRepo = AppDataSource.manager.getRepository(Settings) export const settingsRepo = AppDataSource.manager.getRepository(Settings);
export const getNow = async (): Promise<string> => { export const getNow = async (): Promise<string> => {
const query = await AppDataSource.manager.query( const query = await AppDataSource.manager.query(
"SELECT STRFTIME('%Y-%m-%dT%H:%M:%S','now','localtime') AS now", "SELECT STRFTIME('%Y-%m-%dT%H:%M:%S','now','localtime') AS now"
) );
return query[0].now return query[0].now;
} };

View File

@ -1,8 +1,8 @@
export type DrawerParamList = { export type DrawerParamList = {
Home: {} Home: {};
Settings: {} Settings: {};
Graphs: {} Graphs: {};
Plans: {} Plans: {};
Workouts: {} Workouts: {};
Timer: {} Timer: {};
} };

View File

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

View File

@ -1,11 +1,11 @@
import GymSet from './gym-set' import GymSet from "./gym-set";
export type HomePageParams = { export type HomePageParams = {
Sets: {} Sets: {};
EditSet: { EditSet: {
set: GymSet set: GymSet;
} };
EditSets: { EditSets: {
ids: number[] ids: number[];
} };
} };

View File

@ -1,9 +1,9 @@
import { Item } from './Select' import { Item } from "./Select";
import Settings from './settings' import Settings from "./settings";
export default interface Input<T> { export default interface Input<T> {
name: string name: string;
key: keyof Settings key: keyof Settings;
value?: T value?: T;
items?: Item[] items?: Item[];
} }

View File

@ -1,21 +1,21 @@
import { NativeModules } from 'react-native' import { NativeModules } from "react-native";
import 'react-native-gesture-handler/jestSetup' import "react-native-gesture-handler/jestSetup";
NativeModules.RNViewShot = NativeModules.RNViewShot || { NativeModules.RNViewShot = NativeModules.RNViewShot || {
captureScreen: jest.fn(), captureScreen: jest.fn(),
} };
NativeModules.SettingsModule = NativeModules.SettingsModule || { NativeModules.SettingsModule = NativeModules.SettingsModule || {
ignoringBattery: jest.fn(), ignoringBattery: jest.fn(),
is24: jest.fn(() => Promise.resolve(true)), is24: jest.fn(() => Promise.resolve(true)),
} };
jest.mock('react-native-file-access', () => jest.fn()) jest.mock("react-native-file-access", () => jest.fn());
jest.mock('react-native-share', () => jest.fn()) jest.mock("react-native-share", () => jest.fn());
jest.mock('react-native/Libraries/Animated/NativeAnimatedHelper') jest.mock("react-native/Libraries/Animated/NativeAnimatedHelper");
jest.mock('react-native/Libraries/EventEmitter/NativeEventEmitter') jest.mock("react-native/Libraries/EventEmitter/NativeEventEmitter");
jest.mock('react-native-reanimated', () => { jest.mock("react-native-reanimated", () => {
const Reanimated = require('react-native-reanimated/mock') const Reanimated = require("react-native-reanimated/mock");
Reanimated.default.call = () => {} Reanimated.default.call = () => {};
return Reanimated return Reanimated;
}) });

View File

@ -1,5 +1,5 @@
export enum Metrics { export enum Metrics {
Weight = 'Best weight', Weight = "Best weight",
Volume = 'Volume', Volume = "Volume",
OneRepMax = 'One rep max', OneRepMax = "One rep max",
} }

View File

@ -1,22 +1,22 @@
import { NavigationContainer } from '@react-navigation/native' import { NavigationContainer } from "@react-navigation/native";
import React from 'react' import React from "react";
import { import {
DefaultTheme, DefaultTheme,
MD3DarkTheme, MD3DarkTheme,
Provider as PaperProvider, Provider as PaperProvider,
} from 'react-native-paper' } from "react-native-paper";
import MaterialIcon from 'react-native-vector-icons/MaterialIcons' import MaterialIcon from "react-native-vector-icons/MaterialIcons";
import { ThemeContext } from './use-theme' import { ThemeContext } from "./use-theme";
export const MockProviders = ({ export const MockProviders = ({
children, children,
}: { }: {
children: JSX.Element | JSX.Element[] children: JSX.Element | JSX.Element[];
}) => ( }) => (
<PaperProvider settings={{ icon: (props) => <MaterialIcon {...props} /> }}> <PaperProvider settings={{ icon: (props) => <MaterialIcon {...props} /> }}>
<ThemeContext.Provider <ThemeContext.Provider
value={{ value={{
theme: 'system', theme: "system",
setTheme: jest.fn(), setTheme: jest.fn(),
lightColor: DefaultTheme.colors.primary, lightColor: DefaultTheme.colors.primary,
darkColor: MD3DarkTheme.colors.primary, darkColor: MD3DarkTheme.colors.primary,
@ -27,4 +27,4 @@ export const MockProviders = ({
<NavigationContainer>{children}</NavigationContainer> <NavigationContainer>{children}</NavigationContainer>
</ThemeContext.Provider> </ThemeContext.Provider>
</PaperProvider> </PaperProvider>
) );

View File

@ -1,19 +1,19 @@
import { darkColors, lightColors } from './colors' import { darkColors, lightColors } from "./colors";
export const themeOptions = [ export const themeOptions = [
{ label: 'System', value: 'system' }, { label: "System", value: "system" },
{ label: 'Dark', value: 'dark' }, { label: "Dark", value: "dark" },
{ label: 'Light', value: 'light' }, { label: "Light", value: "light" },
] ];
export const lightOptions = lightColors.map((color) => ({ export const lightOptions = lightColors.map((color) => ({
label: color.name, label: color.name,
value: color.hex, value: color.hex,
color: color.hex, color: color.hex,
})) }));
export const darkOptions = darkColors.map((color) => ({ export const darkOptions = darkColors.map((color) => ({
label: color.name, label: color.name,
value: color.hex, value: color.hex,
color: color.hex, color: color.hex,
})) }));

View File

@ -1,5 +1,5 @@
export enum Periods { export enum Periods {
Weekly = 'This week', Weekly = "This week",
Monthly = 'This month', Monthly = "This month",
Yearly = 'This year', Yearly = "This year",
} }

View File

@ -1,16 +1,16 @@
import GymSet from './gym-set' import GymSet from "./gym-set";
import { Plan } from './plan' import { Plan } from "./plan";
export type PlanPageParams = { export type PlanPageParams = {
PlanList: {} PlanList: {};
EditPlan: { EditPlan: {
plan: Plan plan: Plan;
} };
StartPlan: { StartPlan: {
plan: Plan plan: Plan;
first?: GymSet first?: GymSet;
} };
EditSet: { EditSet: {
set: GymSet set: GymSet;
} };
} };

14
plan.ts
View File

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

View File

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

View File

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

16
time.ts
View File

@ -1,9 +1,9 @@
export const DAYS = [ export const DAYS = [
'Sunday', "Sunday",
'Monday', "Monday",
'Tuesday', "Tuesday",
'Wednesday', "Wednesday",
'Thursday', "Thursday",
'Friday', "Friday",
'Saturday', "Saturday",
] ];

View File

@ -1,7 +1,7 @@
import { DeviceEventEmitter } from 'react-native' import { DeviceEventEmitter } from "react-native";
export const TOAST = 'toast' export const TOAST = "toast";
export function toast(value: string) { export function toast(value: string) {
DeviceEventEmitter.emit(TOAST, { value }) DeviceEventEmitter.emit(TOAST, { value });
} }

View File

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

View File

@ -1,22 +1,22 @@
import { createContext, useContext } from 'react' import { createContext, useContext } from "react";
import { MD3DarkTheme, MD3LightTheme } from 'react-native-paper' import { MD3DarkTheme, MD3LightTheme } from "react-native-paper";
export const ThemeContext = createContext<{ export const ThemeContext = createContext<{
theme: string theme: string;
lightColor: string lightColor: string;
setTheme: (value: string) => void setTheme: (value: string) => void;
setLightColor: (value: string) => void setLightColor: (value: string) => void;
darkColor: string darkColor: string;
setDarkColor: (value: string) => void setDarkColor: (value: string) => void;
}>({ }>({
theme: 'system', theme: "system",
lightColor: MD3DarkTheme.colors.primary, lightColor: MD3DarkTheme.colors.primary,
setTheme: () => null, setTheme: () => null,
setLightColor: () => null, setLightColor: () => null,
darkColor: MD3LightTheme.colors.primary, darkColor: MD3LightTheme.colors.primary,
setDarkColor: () => null, setDarkColor: () => null,
}) });
export function useTheme() { export function useTheme() {
return useContext(ThemeContext) return useContext(ThemeContext);
} }

View File

@ -1,25 +1,25 @@
import { useFocusEffect } from '@react-navigation/native' import { useFocusEffect } from "@react-navigation/native";
import { useCallback, useState } from 'react' import { useCallback, useState } from "react";
import { NativeEventEmitter } from 'react-native' import { NativeEventEmitter } from "react-native";
import { TickEvent } from './TimerPage' import { TickEvent } from "./TimerPage";
export default function useTimer() { export default function useTimer() {
const [minutes, setMinutes] = useState('00') const [minutes, setMinutes] = useState("00");
const [seconds, setSeconds] = useState('00') const [seconds, setSeconds] = useState("00");
useFocusEffect( useFocusEffect(
useCallback(() => { useCallback(() => {
setMinutes('00') setMinutes("00");
setSeconds('00') setSeconds("00");
const emitter = new NativeEventEmitter() const emitter = new NativeEventEmitter();
const listener = emitter.addListener('tick', (event: TickEvent) => { const listener = emitter.addListener("tick", (event: TickEvent) => {
console.log(`${useTimer.name}.tick:`, { event }) console.log(`${useTimer.name}.tick:`, { event });
setMinutes(event.minutes) setMinutes(event.minutes);
setSeconds(event.seconds) setSeconds(event.seconds);
}) });
return listener.remove return listener.remove;
}, []), }, [])
) );
return { minutes, seconds } return { minutes, seconds };
} }

View File

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

View File

@ -1,21 +1,21 @@
import { PermissionsAndroid, Platform } from 'react-native' import { PermissionsAndroid, Platform } from "react-native";
import { Dirs, FileSystem } from 'react-native-file-access' import { Dirs, FileSystem } from "react-native-file-access";
import { toast } from './toast' import { toast } from "./toast";
export const write = async (name: string, data: string) => { export const write = async (name: string, data: string) => {
const filePath = `${Dirs.DocumentDir}/${name}` const filePath = `${Dirs.DocumentDir}/${name}`;
const permission = async () => { const permission = async () => {
if (Platform.OS !== 'android') return true if (Platform.OS !== "android") return true;
const granted = await PermissionsAndroid.request( const granted = await PermissionsAndroid.request(
PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE, PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE
) );
return granted === PermissionsAndroid.RESULTS.GRANTED return granted === PermissionsAndroid.RESULTS.GRANTED;
};
const granted = await permission();
if (!granted) return;
await FileSystem.writeFile(filePath, data);
if (Platform.OS === "android") {
await FileSystem.cpExternal(filePath, name, "downloads");
} }
const granted = await permission() toast(`Downloaded ${name}`);
if (!granted) return };
await FileSystem.writeFile(filePath, data)
if (Platform.OS === 'android') {
await FileSystem.cpExternal(filePath, name, 'downloads')
}
toast(`Downloaded ${name}`)
}