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

View File

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

View File

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

View File

@ -1,11 +1,11 @@
import { useTheme } from '@react-navigation/native'
import * as shape from 'd3-shape'
import { View } from 'react-native'
import { Grid, LineChart, XAxis, YAxis } from 'react-native-svg-charts'
import { CombinedDarkTheme, CombinedDefaultTheme } from './App'
import { MARGIN, PADDING } from './constants'
import GymSet from './gym-set'
import useDark from './use-dark'
import { useTheme } from "@react-navigation/native";
import * as shape from "d3-shape";
import { View } from "react-native";
import { Grid, LineChart, XAxis, YAxis } from "react-native-svg-charts";
import { CombinedDarkTheme, CombinedDefaultTheme } from "./App";
import { MARGIN, PADDING } from "./constants";
import GymSet from "./gym-set";
import useDark from "./use-dark";
export default function Chart({
yData,
@ -13,21 +13,21 @@ export default function Chart({
xData,
yFormat,
}: {
yData: number[]
xData: GymSet[]
xFormat: (value: any, index: number) => string
yFormat: (value: any) => string
yData: number[];
xData: GymSet[];
xFormat: (value: any, index: number) => string;
yFormat: (value: any) => string;
}) {
const { colors } = useTheme()
const dark = useDark()
const { colors } = useTheme();
const dark = useDark();
const axesSvg = {
fontSize: 10,
fill: dark
? CombinedDarkTheme.colors.text
: CombinedDefaultTheme.colors.text,
}
const verticalContentInset = { top: 10, bottom: 10 }
const xAxisHeight = 30
};
const verticalContentInset = { top: 10, bottom: 10 };
const xAxisHeight = 30;
return (
<>
@ -35,7 +35,7 @@ export default function Chart({
style={{
height: 300,
padding: PADDING,
flexDirection: 'row',
flexDirection: "row",
}}
>
<YAxis
@ -66,5 +66,5 @@ export default function Chart({
</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({
title,
@ -8,17 +8,17 @@ export default function ConfirmDialog({
setShow,
onCancel,
}: {
title: string
children: JSX.Element | JSX.Element[] | string
onOk: () => void
show: boolean
setShow: (show: boolean) => void
onCancel?: () => void
title: string;
children: JSX.Element | JSX.Element[] | string;
onOk: () => void;
show: boolean;
setShow: (show: boolean) => void;
onCancel?: () => void;
}) {
const cancel = () => {
setShow(false)
onCancel && onCancel()
}
setShow(false);
onCancel && onCancel();
};
return (
<Portal>
@ -33,5 +33,5 @@ export default function ConfirmDialog({
</Dialog.Actions>
</Dialog>
</Portal>
)
);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,12 +1,12 @@
import React, { useCallback, useMemo, useState } from 'react'
import { View } from 'react-native'
import { Button, Menu, Subheading } from 'react-native-paper'
import { ITEM_PADDING } from './constants'
import React, { useCallback, useMemo, useState } from "react";
import { View } from "react-native";
import { Button, Menu, Subheading } from "react-native-paper";
import { ITEM_PADDING } from "./constants";
export interface Item {
value: string
label: string
color?: string
value: string;
label: string;
color?: string;
}
function Select({
@ -15,31 +15,31 @@ function Select({
items,
label,
}: {
value: string
onChange: (value: string) => void
items: Item[]
label?: string
value: string;
onChange: (value: string) => void;
items: Item[];
label?: string;
}) {
const [show, setShow] = useState(false)
const [show, setShow] = useState(false);
const selected = useMemo(
() => items.find((item) => item.value === value) || items[0],
[items, value],
)
[items, value]
);
const handlePress = useCallback(
(newValue: string) => {
onChange(newValue)
setShow(false)
onChange(newValue);
setShow(false);
},
[onChange],
)
[onChange]
);
return (
<View
style={{
flexDirection: 'row',
alignItems: 'center',
flexDirection: "row",
alignItems: "center",
paddingLeft: ITEM_PADDING,
}}
>
@ -51,7 +51,7 @@ function Select({
<Button
onPress={() => setShow(true)}
style={{
alignSelf: 'flex-start',
alignSelf: "flex-start",
}}
>
{selected?.label}
@ -68,7 +68,7 @@ function Select({
))}
</Menu>
</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 { format } from 'date-fns'
import { useCallback, useMemo } from 'react'
import { Image } from 'react-native'
import { List, Text } from 'react-native-paper'
import { DARK_RIPPLE, LIGHT_RIPPLE } from './constants'
import GymSet from './gym-set'
import { HomePageParams } from './home-page-params'
import Settings from './settings'
import useDark from './use-dark'
import { NavigationProp, useNavigation } from "@react-navigation/native";
import { format } from "date-fns";
import { useCallback, useMemo } from "react";
import { Image } from "react-native";
import { List, Text } from "react-native-paper";
import { DARK_RIPPLE, LIGHT_RIPPLE } from "./constants";
import GymSet from "./gym-set";
import { HomePageParams } from "./home-page-params";
import Settings from "./settings";
import useDark from "./use-dark";
export default function SetItem({
item,
@ -15,66 +15,63 @@ export default function SetItem({
ids,
setIds,
}: {
item: GymSet
onRemove: () => void
settings: Settings
ids: number[]
setIds: (value: number[]) => void
item: GymSet;
onRemove: () => void;
settings: Settings;
ids: number[];
setIds: (value: number[]) => void;
}) {
const dark = useDark()
const navigation = useNavigation<NavigationProp<HomePageParams>>()
const dark = useDark();
const navigation = useNavigation<NavigationProp<HomePageParams>>();
const longPress = useCallback(() => {
if (ids.length > 0) return
setIds([item.id])
}, [ids.length, item.id, setIds])
if (ids.length > 0) return;
setIds([item.id]);
}, [ids.length, item.id, setIds]);
const press = useCallback(() => {
if (ids.length === 0) return navigation.navigate('EditSet', { set: item })
const removing = ids.find((id) => id === item.id)
if (removing) setIds(ids.filter((id) => id !== item.id))
else setIds([...ids, item.id])
}, [ids, item, navigation, setIds])
if (ids.length === 0) return navigation.navigate("EditSet", { set: item });
const removing = ids.find((id) => id === item.id);
if (removing) setIds(ids.filter((id) => id !== item.id));
else setIds([...ids, item.id]);
}, [ids, item, navigation, setIds]);
const backgroundColor = useMemo(() => {
if (!ids.includes(item.id)) return
if (dark) return DARK_RIPPLE
return LIGHT_RIPPLE
}, [dark, ids, item.id])
if (!ids.includes(item.id)) return;
if (dark) return DARK_RIPPLE;
return LIGHT_RIPPLE;
}, [dark, ids, item.id]);
const left = useCallback(() => {
if (!settings.images || !item.image) return null
if (!settings.images || !item.image) return null;
return (
<Image
source={{ uri: item.image }}
style={{ height: 75, width: 75 }}
/>
)
}, [item.image, settings.images])
<Image source={{ uri: item.image }} style={{ height: 75, width: 75 }} />
);
}, [item.image, settings.images]);
const right = useCallback(() => {
if (!settings.showDate) return null
if (!settings.showDate) return null;
return (
<Text
style={{
alignSelf: 'center',
color: dark ? '#909090ff' : '#717171ff',
alignSelf: "center",
color: dark ? "#909090ff" : "#717171ff",
}}
>
{format(new Date(item.created), settings.date || 'P')}
{format(new Date(item.created), settings.date || "P")}
</Text>
)
}, [settings.showDate, item.created, settings.date, dark])
);
}, [settings.showDate, item.created, settings.date, dark]);
return (
<List.Item
onPress={press}
title={item.name}
description={`${item.reps} x ${item.weight}${item.unit || 'kg'}`}
description={`${item.reps} x ${item.weight}${item.unit || "kg"}`}
onLongPress={longPress}
style={{ backgroundColor }}
left={left}
right={right}
/>
)
);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,60 +1,60 @@
import { useFocusEffect } from '@react-navigation/native'
import React, { useCallback, useMemo, useState } from 'react'
import { Dimensions, NativeModules, View } from 'react-native'
import { Button, Text, useTheme } from 'react-native-paper'
import { ProgressCircle } from 'react-native-svg-charts'
import AppFab from './AppFab'
import { MARGIN, PADDING } from './constants'
import { settingsRepo } from './db'
import DrawerHeader from './DrawerHeader'
import Settings from './settings'
import useTimer from './use-timer'
import { useFocusEffect } from "@react-navigation/native";
import React, { useCallback, useMemo, useState } from "react";
import { Dimensions, NativeModules, View } from "react-native";
import { Button, Text, useTheme } from "react-native-paper";
import { ProgressCircle } from "react-native-svg-charts";
import AppFab from "./AppFab";
import { MARGIN, PADDING } from "./constants";
import { settingsRepo } from "./db";
import DrawerHeader from "./DrawerHeader";
import Settings from "./settings";
import useTimer from "./use-timer";
export interface TickEvent {
minutes: string
seconds: string
minutes: string;
seconds: string;
}
export default function TimerPage() {
const { minutes, seconds } = useTimer()
const [settings, setSettings] = useState<Settings>()
const { colors } = useTheme()
const { minutes, seconds } = useTimer();
const [settings, setSettings] = useState<Settings>();
const { colors } = useTheme();
useFocusEffect(
useCallback(() => {
settingsRepo.findOne({ where: {} }).then(setSettings)
}, []),
)
settingsRepo.findOne({ where: {} }).then(setSettings);
}, [])
);
const stop = () => {
NativeModules.AlarmModule.stop()
}
NativeModules.AlarmModule.stop();
};
const add = async () => {
console.log(`${TimerPage.name}.add:`, settings)
NativeModules.AlarmModule.add()
}
console.log(`${TimerPage.name}.add:`, settings);
NativeModules.AlarmModule.add();
};
const progress = useMemo(() => {
return (Number(minutes) * 60 + Number(seconds)) / 210
}, [minutes, seconds])
return (Number(minutes) * 60 + Number(seconds)) / 210;
}, [minutes, seconds]);
const left = useMemo(() => {
return Dimensions.get('screen').width * 0.5 - 60
}, [])
return Dimensions.get("screen").width * 0.5 - 60;
}, []);
return (
<>
<DrawerHeader name='Timer' />
<DrawerHeader name="Timer" />
<View style={{ flexGrow: 1, padding: PADDING }}>
<View
style={{
flex: 1,
justifyContent: 'center',
alignItems: 'center',
justifyContent: "center",
alignItems: "center",
}}
>
<Text style={{ fontSize: 70, position: 'absolute' }}>
<Text style={{ fontSize: 70, position: "absolute" }}>
{minutes}:{seconds}
</Text>
<ProgressCircle
@ -62,14 +62,14 @@ export default function TimerPage() {
progress={progress}
strokeWidth={10}
progressColor={colors.primary}
backgroundColor={colors.primary + '80'}
backgroundColor={colors.primary + "80"}
/>
</View>
</View>
<Button onPress={add} style={{ position: 'absolute', top: '82%', left }}>
<Button onPress={add} style={{ position: "absolute", top: "82%", left }}>
Add 1 min
</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 { format } from 'date-fns'
import { useEffect, useMemo, useState } from 'react'
import { View } from 'react-native'
import { FileSystem } from 'react-native-file-access'
import { IconButton, List } from 'react-native-paper'
import Share from 'react-native-share'
import { captureScreen } from 'react-native-view-shot'
import Chart from './Chart'
import { GraphsPageParams } from './GraphsPage'
import Select from './Select'
import StackHeader from './StackHeader'
import { PADDING } from './constants'
import { setRepo } from './db'
import GymSet from './gym-set'
import { Metrics } from './metrics'
import { Periods } from './periods'
import Volume from './volume'
import { RouteProp, useRoute } from "@react-navigation/native";
import { format } from "date-fns";
import { useEffect, useMemo, useState } from "react";
import { View } from "react-native";
import { FileSystem } from "react-native-file-access";
import { IconButton, List } from "react-native-paper";
import Share from "react-native-share";
import { captureScreen } from "react-native-view-shot";
import Chart from "./Chart";
import { GraphsPageParams } from "./GraphsPage";
import Select from "./Select";
import StackHeader from "./StackHeader";
import { PADDING } from "./constants";
import { setRepo } from "./db";
import GymSet from "./gym-set";
import { Metrics } from "./metrics";
import { Periods } from "./periods";
import Volume from "./volume";
export default function ViewGraph() {
const { params } = useRoute<RouteProp<GraphsPageParams, 'ViewGraph'>>()
const [weights, setWeights] = useState<GymSet[]>()
const [volumes, setVolumes] = useState<Volume[]>()
const [metric, setMetric] = useState(Metrics.Weight)
const [period, setPeriod] = useState(Periods.Monthly)
const { params } = useRoute<RouteProp<GraphsPageParams, "ViewGraph">>();
const [weights, setWeights] = useState<GymSet[]>();
const [volumes, setVolumes] = useState<Volume[]>();
const [metric, setMetric] = useState(Metrics.Weight);
const [period, setPeriod] = useState(Periods.Monthly);
useEffect(() => {
let difference = '-7 days'
if (period === Periods.Monthly) difference = '-1 months'
else if (period === Periods.Yearly) difference = '-1 years'
let group = '%Y-%m-%d'
if (period === Periods.Yearly) group = '%Y-%m'
let difference = "-7 days";
if (period === Periods.Monthly) difference = "-1 months";
else if (period === Periods.Yearly) difference = "-1 years";
let group = "%Y-%m-%d";
if (period === Periods.Yearly) group = "%Y-%m";
const builder = setRepo
.createQueryBuilder()
.select("STRFTIME('%Y-%m-%d', created)", 'created')
.addSelect('unit')
.where('name = :name', { name: params.best.name })
.andWhere('NOT hidden')
.select("STRFTIME('%Y-%m-%d', created)", "created")
.addSelect("unit")
.where("name = :name", { name: params.best.name })
.andWhere("NOT hidden")
.andWhere("DATE(created) >= DATE('now', 'weekday 0', :difference)", {
difference,
})
.groupBy('name')
.addGroupBy(`STRFTIME('${group}', created)`)
.groupBy("name")
.addGroupBy(`STRFTIME('${group}', created)`);
switch (metric) {
case Metrics.Weight:
builder
.addSelect('ROUND(MAX(weight), 2)', 'weight')
.addSelect("ROUND(MAX(weight), 2)", "weight")
.getRawMany()
.then(setWeights)
break
.then(setWeights);
break;
case Metrics.Volume:
builder
.addSelect('ROUND(SUM(weight * reps), 2)', 'value')
.addSelect("ROUND(SUM(weight * reps), 2)", "value")
.getRawMany()
.then(setVolumes)
break
.then(setVolumes);
break;
default:
// Brzycki formula https://en.wikipedia.org/wiki/One-repetition_maximum#Brzycki
builder
.addSelect(
'ROUND(MAX(weight / (1.0278 - 0.0278 * reps)), 2)',
'weight',
"ROUND(MAX(weight / (1.0278 - 0.0278 * reps)), 2)",
"weight"
)
.getRawMany()
.then((newWeights) => {
console.log({ weights: newWeights })
setWeights(newWeights)
})
console.log({ weights: newWeights });
setWeights(newWeights);
});
}
}, [params.best.name, metric, period])
}, [params.best.name, metric, period]);
const charts = useMemo(() => {
if (
@ -75,21 +75,23 @@ export default function ViewGraph() {
(metric === Metrics.Weight && 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) {
return (
<Chart
yData={volumes.map((v) => v.value)}
yFormat={(value: number) =>
`${value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')}${
volumes[0].unit || 'kg'
}`}
`${value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",")}${
volumes[0].unit || "kg"
}`
}
xData={weights}
xFormat={(_value, index) =>
format(new Date(weights[index].created), 'd/M')}
format(new Date(weights[index].created), "d/M")
}
/>
)
);
}
return (
@ -98,10 +100,11 @@ export default function ViewGraph() {
yFormat={(value) => `${value}${weights?.[0].unit}`}
xData={weights || []}
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 (
<>
@ -109,19 +112,20 @@ export default function ViewGraph() {
<IconButton
onPress={() =>
captureScreen().then(async (uri) => {
const base64 = await FileSystem.readFile(uri, 'base64')
const url = `data:image/jpeg;base64,${base64}`
const base64 = await FileSystem.readFile(uri, "base64");
const url = `data:image/jpeg;base64,${base64}`;
Share.open({
type: 'image/jpeg',
type: "image/jpeg",
url,
})
})}
icon='share'
});
})
}
icon="share"
/>
</StackHeader>
<View style={{ padding: PADDING }}>
<Select
label='Metric'
label="Metric"
items={[
{ value: Metrics.Volume, label: Metrics.Volume },
{ value: Metrics.OneRepMax, label: Metrics.OneRepMax },
@ -134,7 +138,7 @@ export default function ViewGraph() {
value={metric}
/>
<Select
label='Period'
label="Period"
items={[
{ value: Periods.Weekly, label: Periods.Weekly },
{ value: Periods.Monthly, label: Periods.Monthly },
@ -146,5 +150,5 @@ export default function ViewGraph() {
{charts}
</View>
</>
)
);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

22
db.ts
View File

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

View File

@ -1,8 +1,8 @@
export type DrawerParamList = {
Home: {}
Settings: {}
Graphs: {}
Plans: {}
Workouts: {}
Timer: {}
}
Home: {};
Settings: {};
Graphs: {};
Plans: {};
Workouts: {};
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 {
@PrimaryGeneratedColumn()
id?: number
id?: number;
@Column('text')
name: string
@Column("text")
name: string;
@Column('int')
reps: number
@Column("int")
reps: number;
@Column('int')
weight: number
@Column("int")
weight: number;
@Column('int')
sets = 3
@Column("int")
sets = 3;
@Column('int')
minutes = 3
@Column("int")
minutes = 3;
@Column('int')
seconds = 30
@Column("int")
seconds = 30;
@Column('boolean')
hidden = false
@Column("boolean")
hidden = false;
@Column('text')
created: string
@Column("text")
created: string;
@Column('text')
unit: string
@Column("text")
unit: string;
@Column('text')
image: string
@Column("text")
image: string;
@Column('text')
steps?: string
@Column("text")
steps?: string;
}
export const defaultSet: GymSet = {
created: '',
name: '',
image: '',
created: "",
name: "",
image: "",
hidden: false,
minutes: 3,
seconds: 30,
reps: 0,
sets: 0,
unit: 'kg',
unit: "kg",
weight: 0,
}
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,16 +1,16 @@
import GymSet from './gym-set'
import { Plan } from './plan'
import GymSet from "./gym-set";
import { Plan } from "./plan";
export type PlanPageParams = {
PlanList: {}
PlanList: {};
EditPlan: {
plan: Plan
}
plan: Plan;
};
StartPlan: {
plan: Plan
first?: GymSet
}
plan: Plan;
first?: GymSet;
};
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 {
@PrimaryGeneratedColumn()
id?: number
id?: number;
@Column('text')
days: string
@Column("text")
days: string;
@Column('text')
workouts: string
@Column("text")
workouts: string;
}

View File

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

View File

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

16
time.ts
View File

@ -1,9 +1,9 @@
export const DAYS = [
'Sunday',
'Monday',
'Tuesday',
'Wednesday',
'Thursday',
'Friday',
'Saturday',
]
"Sunday",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"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) {
DeviceEventEmitter.emit(TOAST, { value })
DeviceEventEmitter.emit(TOAST, { value });
}

View File

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

View File

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

View File

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

View File

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

View File

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